Passed
Push — master ( 41f848...de078e )
by Corey
05:23 queued 02:26
created

UserBehavior::parseBehaviorData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 13
rs 10
cc 3
nc 3
nop 1
1
<?php
2
3
namespace common\models;
4
5
use Yii;
6
use \common\interfaces\TimeInterface;
7
use \common\interfaces\BehaviorInterface;
8
use \common\interfaces\UserBehaviorInterface;
9
use \common\components\ActiveRecord;
10
use yii\db\Query;
11
use yii\helpers\ArrayHelper as AH;
12
use \DateTime;
13
use \DateTimeZone;
14
use yii\db\Expression;
15
16
/**
17
 * This is the model class for table "user_behavior_link".
18
 *
19
 * @property integer $id
20
 * @property integer $user_id
21
 * @property integer $behavior_id
22
 * @property integer $category_id
23
 * @property string $date
24
 *
25
 * @property Behavior $user
26
 */
27
class UserBehavior extends ActiveRecord implements UserBehaviorInterface
28
{
29
  private $time;
30
  private $behavior;
31
32
  public function __construct(BehaviorInterface $behavior, TimeInterface $time, $config = []) {
33
    $this->behavior = $behavior;
34
    $this->time = $time;
35
    parent::__construct($config);
36
  }
37
38
  /**
39
   * @inheritdoc
40
   */
41
  public static function tableName()
42
  {
43
    return 'user_behavior_link';
44
  }
45
46
  /**
47
   * @inheritdoc
48
   */
49
  public function rules()
50
  {
51
    return [
52
      [['user_id', 'behavior_id', 'category_id', 'date'], 'required'],
53
      [['user_id', 'behavior_id', 'category_id'], 'integer'],
54
      //[['date'], 'string']
55
    ];
56
  }
57
58
  /**
59
   * @inheritdoc
60
   */
61
  public function attributeLabels()
62
  {
63
    return [
64
      'id'        => 'ID',
65
      'date'      => 'Date',
66
      'user_id'   => 'User ID',
67
      'behavior_id' => 'Behavior ID',
68
      'category_id' => 'Category ID',
69
    ];
70
  }
71
72
  /**
73
   * @return \yii\db\ActiveQuery
74
   */
75
  public function getUser()
76
  {
77
    return $this->hasOne(\common\models\User::class, ['id' => 'user_id']);
78
  }
79
80
  public function getPastCheckinDates() {
81
    $past_checkin_dates = [];
82
    $query = new Query;
83
    $query->params = [":user_id" => Yii::$app->user->id];
84
    $query->select("date")
85
      ->from('user_behavior_link l')
86
      ->groupBy('date, user_id')
87
      ->having('user_id = :user_id');
88
    $temp_dates = $query->all();
89
    foreach($temp_dates as $temp_date) {
90
      $past_checkin_dates[] = $this->time->convertUTCToLocal($temp_date['date']);
91
    }
92
93
    return $past_checkin_dates;
94
  }
95
96
  public function getUserBehaviorsWithCategory($checkin_date) {
97
    list($start, $end) = $this->time->getUTCBookends($checkin_date);
98
99
    $query = new Query;
100
    $query->select('*')
101
      ->from('user_behavior_link')
102
      ->orderBy('behavior_id')
103
      ->where("user_id=:user_id
104
          AND date > :start_date
105
          AND date < :end_date",
106
      [
107
        ":user_id" => Yii::$app->user->id, 
108
        ":start_date" => $start, 
109
        ":end_date" => $end
110
      ]);
111
112
    $user_behaviors = self::decorate($query->all());
113
    return AH::map($user_behaviors, 'id', function($a) { return $a['behavior']['name']; }, function($b) {return $b['behavior']['category_id']; });
0 ignored issues
show
Bug introduced by
$user_behaviors of type common\models\an is incompatible with the type array expected by parameter $array of yii\helpers\BaseArrayHelper::map(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
    return AH::map(/** @scrutinizer ignore-type */ $user_behaviors, 'id', function($a) { return $a['behavior']['name']; }, function($b) {return $b['behavior']['category_id']; });
Loading history...
114
  }
115
116
  public function getCheckinBreakdown(int $period = 30) {
117
    $datetimes = $this->time->getDateTimesInPeriod($period);
0 ignored issues
show
Bug introduced by
The method getDateTimesInPeriod() does not exist on common\interfaces\TimeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to common\interfaces\TimeInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

117
    /** @scrutinizer ignore-call */ 
118
    $datetimes = $this->time->getDateTimesInPeriod($period);
Loading history...
118
    $key = "checkins_".Yii::$app->user->id."_{$period}_".$this->time->getLocalDate();
119
    $checkins = Yii::$app->cache->get($key);
120
121
    if($checkins === false) {
122
      $checkins = [];
123
      foreach($datetimes as $datetime) {
124
        $behaviors = self::decorate($this->getBehaviorsWithCounts($datetime));
125
        $checkins[$datetime->format('Y-m-d')] = $this->getBehaviorsByCategory($behaviors);
0 ignored issues
show
Bug introduced by
$behaviors of type common\models\an is incompatible with the type array expected by parameter $decorated_behaviors of common\models\UserBehavi...etBehaviorsByCategory(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

125
        $checkins[$datetime->format('Y-m-d')] = $this->getBehaviorsByCategory(/** @scrutinizer ignore-type */ $behaviors);
Loading history...
126
      }
127
128
      Yii::$app->cache->set($key, $checkins, 60*60*24);
129
    }
130
131
    return $checkins;
132
  }
133
134
  /**
135
   * @param integer $limit the desired number of behaviors
136
   * @return array a list of the most-selected behaviors
137
   */
138
  public function getTopBehaviors(int $limit = 5) {
139
    return self::decorate($this->getBehaviorsWithCounts($limit));
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::decorate($t...iorsWithCounts($limit)) returns the type common\models\an which is incompatible with the documented return type array.
Loading history...
140
  }
141
142
  /**
143
   * @param array $decorated_behaviors an array of behaviors ran through self::decorate()
144
   * @return array a list of categories and the number of selected behaviors in each category
145
   */
146
  public function getBehaviorsByCategory(array $decorated_behaviors) {
147
    $arr = array_reduce($decorated_behaviors, function($acc, $row) {
148
      $cat_id = $row['category_id'];
149
      if(array_key_exists($cat_id, $acc)) {
150
        $acc[$cat_id]['count'] += $row['count'];
151
      } else {
152
        $acc[$cat_id] = [
153
          'name'      => $row['category']['name'],
154
          'count'     => $row['count'],
155
          'color'     => \common\models\Category::$colors[$cat_id]['color'],
156
          'highlight' => \common\models\Category::$colors[$cat_id]['highlight'],
157
        ];
158
      }
159
      return $acc;
160
    }, []);
161
    ksort($arr);
162
    return $arr;
163
  }
164
165
  /*
166
   * @param integer|\DateTime $limit a limit for the number of behaviors to
167
   * return if an integer is supplied. If a \DateTime object is given instead,
168
   * then it adds a start and end date range in the WHERE clause of the query.
169
   * @return array a list of the user's behaviors with a count of each behavior's
170
   * occurence, sorted in descending order of occurrence.
171
   */
172
  public function getBehaviorsWithCounts($limit = null) {
173
    $query = new Query;
174
    $query->params = [":user_id" => Yii::$app->user->id];
175
    $query->select("user_id, behavior_id, category_id, COUNT(id) as count")
176
      ->from('user_behavior_link')
177
      ->groupBy('behavior_id, category_id, user_id')
178
      ->having('user_id = :user_id')
179
      ->orderBy('count DESC');
180
181
    if($limit instanceof \DateTime) {
182
      list($start, $end) = $this->time->getUTCBookends($limit->format('Y-m-d'));
183
      $query->params += [':start_date' => $start, ':end_date' => $end];
184
      $query->where('user_id=:user_id AND date > :start_date AND date <= :end_date');
185
    } else if(is_int($limit)) {
186
      $query->limit($limit);
187
    }
188
    return $query->all();
189
  }
190
191
  /**
192
   * Returns an array of behaviors selected by the given user on the given date.
193
   * NOTE: this function is designed to return data in the same format that is returned by using yii\helpers\ArrayHelper::index(\common\models\Behavior::$behaviors, 'name', "category_id"). This facilitates generation of the CheckinForm and allows us to easily merge these two datasets together.
194
   * @param integer $user_id
195
   * @param string|null $local_date
196
   * @return array
197
   */
198
  public function getByDate(int $user_id, $local_date = null) {
199
    if(is_null($local_date)) $local_date = $this->time->getLocalDate();
200
201
    $behaviors = $this->getBehaviorData($user_id, $local_date);
202
    $behaviors = self::decorate($behaviors);
203
    return $this->parseBehaviorData($behaviors);
204
  }
205
206
  /**
207
   * Adds the respective Category and Behavior to each UserBehavior in the given array.
208
   * @param array of UserBehaviors
0 ignored issues
show
Bug introduced by
The type common\models\of was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
209
   * @return an array of decorated UserBehaviors, each with an added Category and Behavior
0 ignored issues
show
Bug introduced by
The type common\models\an was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
210
   */
211
  public static function decorate(array $uo) {
212
    foreach($uo as &$o) {
213
      $behavior = \common\models\Behavior::getBehavior('id', $o['behavior_id']);
214
      $category = \common\models\Category::getCategory('id', $o['category_id']);
215
      // if this behavior does not have a valid behavior or category, something
216
      // is weird and we don't want to do this halfway.
217
      if($behavior && $category) {
218
        $o['behavior'] = $behavior;
219
        $o['category'] = $category;
220
      }
221
    }
222
    return $uo;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $uo returns the type array which is incompatible with the documented return type common\models\an.
Loading history...
223
  }
224
225
  // TODO: this should probably be a private method...but unit testing is hard
226
  public function getBehaviorData(int $user_id, $local_date) {
0 ignored issues
show
Unused Code introduced by
The parameter $user_id is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

226
  public function getBehaviorData(/** @scrutinizer ignore-unused */ int $user_id, $local_date) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
227
    list($start, $end) = $this->time->getUTCBookends($local_date);
228
229
    return $this->find()
230
      ->where("user_id=:user_id
231
      AND date > :start_date
232
      AND date < :end_date",
233
    [
234
      "user_id" => Yii::$app->user->id,
235
      ':start_date' => $start,
236
      ":end_date" => $end
237
    ])
238
    ->asArray()
239
    ->all();
240
  }
241
242
  // TODO: this should probably be a private method...but unit testing is hard
243
  public function parseBehaviorData($behaviors) {
244
    if(!$behaviors) return [];
245
246
    $bhvrs_by_cat = [];
247
    foreach($behaviors as $behavior) {
248
      $indx = $behavior['category_id'];
249
250
      $bhvrs_by_cat[$indx][$behavior['behavior']['name']] = [
251
        "id" => $behavior['behavior_id'],
252
        "name"=>$behavior['behavior']['name']];
253
    }
254
255
    return $bhvrs_by_cat;
256
  }
257
}
258