Completed
Push — master ( 443011...e94210 )
by Timur
01:24
created

getAchievementsWithProgressFor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
nop 2
1
<?php
2
3
namespace Laravel\Achievements;
4
5
use Carbon\Carbon;
6
use Illuminate\Contracts\Config\Repository;
7
use Illuminate\Database\Eloquent\Collection;
8
use Illuminate\Support\Facades\Event;
9
use Zurbaev\Achievements\Achievement;
10
use Zurbaev\Achievements\AchievementCriteria;
11
use Zurbaev\Achievements\AchievementCriteriaProgress;
12
use Zurbaev\Achievements\Contracts\AchievementsStorageInterface;
13
14
class AchievementsStorage implements AchievementsStorageInterface
15
{
16
    const MODEL_ACHIEVEMENT = 'achievement';
17
    const MODEL_CRITERIA = 'criteria';
18
19
    /**
20
     * @var Repository
21
     */
22
    protected $config;
23
24
    /**
25
     * AchievementsStorage constructor.
26
     *
27
     * @param Repository $config
28
     */
29
    public function __construct(Repository $config)
30
    {
31
        $this->config = $config;
32
    }
33
34
    /**
35
     * Returns actual class name from config.
36
     *
37
     * @param string $section
38
     * @param string $type
39
     *
40
     * @return string
41
     */
42
    protected function getConfigurableClassName(string $section, string $type)
43
    {
44
        return $this->config->get('achievements.'.$section.'.'.$type);
45
    }
46
47
    /**
48
     * Injects model class name into given callback.
49
     *
50
     * @param string   $type
51
     * @param callable $callback
52
     *
53
     * @return mixed|string
54
     */
55
    protected function getModelClass(string $type, callable $callback)
56
    {
57
        return call_user_func($callback, $this->getConfigurableClassName('models', $type));
58
    }
59
60
    /**
61
     * Dispatches new Achievements event.
62
     *
63
     * @param string $type
64
     * @param array $args
65
     * @return mixed
66
     */
67
    protected function dispatchEvent(string $type, array $args = [])
68
    {
69
        $className = $this->getConfigurableClassName('events', $type);
70
71
        return Event::dispatch(new $className(...$args));
72
    }
73
74
    /**
75
     * Fetches given owner's criterias by given type.
76
     *
77
     * @param mixed  $owner
78
     * @param string $type
79
     *
80
     * @return array
81
     */
82
    public function getOwnerCriteriasByType($owner, string $type)
83
    {
84
        $criterias = $this->getCriteriasByType($type);
85
86
        if (!count($criterias)) {
87
            return [];
88
        }
89
90
        $ownerCriteriaProgress = $this->getOwnerCriteriasProgress($owner, function ($query) use ($type) {
91
            $query->where('type', $type);
92
        });
93
94
        return $criterias->map(function ($criteria) use ($ownerCriteriaProgress) {
95
            /** @var AchievementCriteriaModel $criteria */
96
97
            return $this->transformCriteriaWithProgress($criteria, $ownerCriteriaProgress->get($criteria->id));
98
        })->toArray();
99
    }
100
101
    /**
102
     * Loads criteria progress for given owner, applies given query callback
103
     * and returns array of AchievementCriteria objects transformed from AchievementCriteriaModels.
104
     *
105
     * @param mixed    $owner
106
     * @param callable $callback
107
     *
108
     * @return \Illuminate\Support\Collection
109
     */
110
    protected function getOwnerCriteriasProgress($owner, callable $callback)
111
    {
112
        $query = $owner->achievementCriterias();
113
114
        call_user_func_array($callback, [$query]);
115
116
        return $this->transformOwnerCriteriasToProgress($query->get());
117
    }
118
119
    /**
120
     * Loads achievement criterias with given type.
121
     *
122
     * @param string $type
123
     *
124
     * @return \Illuminate\Database\Eloquent\Collection
125
     */
126
    protected function getCriteriasByType(string $type)
127
    {
128
        return $this->getModelClass(static::MODEL_CRITERIA, function (string $className) use ($type) {
129
            return $className::where('type', $type)->get();
130
        });
131
    }
132
133
    /**
134
     * Transforms AchievementCriteriaModel collection int AchievementCriteriaProgress collection.
135
     *
136
     * @param Collection $criterias
137
     *
138
     * @return \Illuminate\Support\Collection
139
     */
140
    protected function transformOwnerCriteriasToProgress(Collection $criterias)
141
    {
142
        return $criterias->keyBy('id')->map(function ($criteria) {
143
            /** @var AchievementCriteriaModel $criteria */
144
145
            return new AchievementCriteriaProgress(
146
                intval($criteria->pivot->value),
147
                false,
148
                intval($criteria->pivot->completed) === 1,
149
                is_string($criteria->pivot->progress_data) ? json_decode($criteria->pivot->progress_data, true) : []
150
            );
151
        });
152
    }
153
154
    /**
155
     * Transforms AchievementCriteriaModel to AchievementCriteria object and attaches progress data.
156
     *
157
     * @param AchievementCriteriaModel         $criteria
158
     * @param AchievementCriteriaProgress|null $progress
159
     *
160
     * @return AchievementCriteria
161
     */
162
    protected function transformCriteriaWithProgress($criteria, AchievementCriteriaProgress $progress = null)
163
    {
164
        $data = [
165
            'id' => $criteria->id,
166
            'achievement_id' => $criteria->achievement_id,
167
            'type' => $criteria->type,
168
            'name' => $criteria->name,
169
            'requirements' => $criteria->requirements ?? [],
170
            'max_value' => $criteria->max_value,
171
        ];
172
173
        if (!is_null($progress)) {
174
            $data['progress'] = [
175
                'value' => $progress->value,
176
                'changed' => false,
177
                'completed' => $progress->completed,
178
                'data' => $progress->data,
179
            ];
180
        }
181
182
        return new AchievementCriteria($data);
183
    }
184
185
    /**
186
     * Returns list of criterias' achievements.
187
     *
188
     * @param array $criterias
189
     *
190
     * @return array
191
     */
192
    public function getAchievementsByCriterias(array $criterias)
193
    {
194
        $achievementIds = array_map(function (AchievementCriteria $criteria) {
195
            return $criteria->achievementId();
196
        }, $criterias);
197
198
        $achievements = $this->getAchievementsByIds(array_unique($achievementIds));
199
200
        if (!count($achievements)) {
201
            return [];
202
        }
203
204
        return $this->transformAchievements($achievements, $criterias)->toArray();
205
    }
206
207
    /**
208
     * Loads collection of achievements by IDs.
209
     *
210
     * @param array $achievementIds
211
     *
212
     * @return mixed
213
     */
214
    public function getAchievementsByIds(array $achievementIds)
215
    {
216
        return $this->getModelClass(static::MODEL_ACHIEVEMENT, function (string $className) use ($achievementIds) {
217
            return $className::whereIn('id', array_unique($achievementIds))->get();
218
        });
219
    }
220
221
    /**
222
     * Converts collection of AchievementModel objects to array of Achievement objects.
223
     *
224
     * @param Collection $achievements
225
     * @param bool       $reloadCriteriasRelation = true
226
     *
227
     * @return array
228
     */
229
    public function convertAchievementModelsWithCriterias(Collection $achievements, bool $reloadCriteriasRelation = true)
230
    {
231
        if ($reloadCriteriasRelation) {
232
            $achievements->load('criterias');
233
        }
234
235
        return $achievements->map([$this, 'convertAchievementModelWithCriterias'])->toArray();
236
    }
237
238
    /**
239
     * Converts single AchievementModel to Achievement object.
240
     *
241
     * @param AchievementModel $achievement
242
     *
243
     * @return Achievement
244
     */
245
    public function convertAchievementModelWithCriterias($achievement)
246
    {
247
        $criterias = $achievement->criterias->map(function ($criteria) {
248
            /** @var AchievementCriteriaModel $criteria */
249
250
            return $this->transformCriteriaWithProgress($criteria);
251
        });
252
253
        return $this->transformSingleAchievement($achievement, $criterias->toArray());
254
    }
255
256
    /**
257
     * Transforms AchievementModel collection to Achievement collection and applies appropriate criterias.
258
     *
259
     * @param Collection $achievements
260
     * @param array      $criterias
261
     *
262
     * @return \Illuminate\Support\Collection
263
     */
264
    protected function transformAchievements(Collection $achievements, array $criterias)
265
    {
266
        return $achievements->map(function ($achievement) use ($criterias) {
267
            /** @var AchievementModel $achievement */
268
269
            return $this->transformSingleAchievement($achievement, $criterias);
270
        });
271
    }
272
273
    /**
274
     * Transforms single AchievementModel object to Achievement object with appropriate criterias.
275
     *
276
     * @param AchievementModel $achievement
277
     * @param array            $criterias
278
     *
279
     * @return Achievement
280
     */
281
    protected function transformSingleAchievement($achievement, array $criterias)
282
    {
283
        $achievementCriterias = array_filter($criterias, function (AchievementCriteria $criteria) use ($achievement) {
284
            return $criteria->achievementId() === $achievement->id;
285
        });
286
287
        // Since we're dealing with owner-related criterias (progress exists if owner has any value),
288
        // we can simply count completed criterias & determine if achievement has been completed.
289
        $completedCriteriasCount = array_sum(
290
            array_map(function (AchievementCriteria $criteria) {
291
                return $criteria->completed() ? 1 : 0;
292
            }, $achievementCriterias)
293
        );
294
295
        return new Achievement([
296
            'id' => $achievement->id,
297
            'name' => $achievement->name,
298
            'description' => $achievement->description,
299
            'points' => $achievement->points,
300
            'completed' => count($achievementCriterias) === $completedCriteriasCount,
301
            'criterias' => $achievementCriterias,
302
        ]);
303
    }
304
305
    /**
306
     * Extracts single Achievement from given list for given criteria.
307
     *
308
     * @param AchievementCriteria $criteria
309
     * @param array               $achievements
310
     *
311
     * @return Achievement
312
     */
313
    public function getAchievementForCriteria(AchievementCriteria $criteria, array $achievements)
314
    {
315
        $collection = collect($achievements);
316
317
        $index = $collection->search(function (Achievement $achievement) use ($criteria) {
318
            return $achievement->id() === $criteria->achievementId();
319
        });
320
321
        if ($index === false) {
322
            throw new \InvalidArgumentException('Achievement for criteria #'.$criteria->id().' was not found.');
323
        }
324
325
        return $collection->get($index);
326
    }
327
328
    /**
329
     * Loads achievements with progresses for given owner.
330
     *
331
     * @param mixed $owner
332
     * @param array $achievementIds
333
     *
334
     * @return array
335
     */
336
    public function getAchievementsWithProgressFor($owner, array $achievementIds)
337
    {
338
        $criterias = $this->getCriteriasByAchievementIds($achievementIds);
339
340
        if (!count($criterias)) {
341
            return [];
342
        }
343
344
        $ownerCriteriaProgress = $this->getOwnerCriteriasProgress($owner, function ($query) use ($criterias) {
345
            $query->whereIn('achievement_criteria_model_id', $criterias->pluck('id'));
346
        });
347
348
        $achievementsCriterias = $criterias->map(function ($criteria) use ($ownerCriteriaProgress) {
349
            /** @var AchievementCriteriaModel $criteria */
350
351
            return $this->transformCriteriaWithProgress($criteria, $ownerCriteriaProgress->get($criteria->id));
352
        });
353
354
        return $this->getAchievementsByCriterias($achievementsCriterias->toArray());
355
    }
356
357
    /**
358
     * Loads criterias by achievement IDs.
359
     *
360
     * @param array $achievementIds
361
     *
362
     * @return \Illuminate\Database\Eloquent\Collection
363
     */
364
    public function getCriteriasByAchievementIds(array $achievementIds)
365
    {
366
        return $this->getModelClass(static::MODEL_CRITERIA, function (string $className) use ($achievementIds) {
367
            return $className::whereIn('achievement_id', $achievementIds)->get();
368
        });
369
    }
370
371
    /**
372
     * Saves criteria progress for given owner.
373
     *
374
     * @param mixed                       $owner
375
     * @param AchievementCriteria         $criteria
376
     * @param Achievement                 $achievement
377
     * @param AchievementCriteriaProgress $progress
378
     *
379
     * @return mixed
380
     */
381
    public function setCriteriaProgressUpdated($owner, AchievementCriteria $criteria, Achievement $achievement, AchievementCriteriaProgress $progress)
382
    {
383
        $owner->achievementCriterias()->syncWithoutDetaching([
384
            $criteria->id() => [
385
                'value' => $progress->value,
386
                'completed' => $progress->completed,
387
                'progress_data' => json_encode($progress->data),
388
            ],
389
        ]);
390
391
        $this->dispatchEvent('criteria_updated', [$owner, $criteria, $achievement, $progress]);
392
393
        return true;
394
    }
395
396
    /**
397
     * Saves given achievements completeness state for given owner.
398
     *
399
     * @param mixed $owner
400
     * @param array $achievements
401
     *
402
     * @return mixed
403
     */
404
    public function setAchievementsCompleted($owner, array $achievements)
405
    {
406
        $now = Carbon::now();
407
        $patch = [];
408
409
        foreach ($achievements as $achievement) {
410
            $patch[$achievement->id()] = ['completed_at' => $now];
411
        }
412
413
        $owner->achievements()->syncWithoutDetaching($patch);
414
415
        $this->dispatchEvent('achievements_completed', [$owner, $achievements]);
416
417
        return true;
418
    }
419
}
420