Issues (28)

src/Reactant/Models/Reactant.php (2 issues)

1
<?php
2
3
/*
4
 * This file is part of Laravel Love.
5
 *
6
 * (c) Anton Komarev <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Cog\Laravel\Love\Reactant\Models;
15
16
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
17
use Cog\Contracts\Love\Reactant\Exceptions\NotAssignedToReactable;
18
use Cog\Contracts\Love\Reactant\Models\Reactant as ReactantInterface;
19
use Cog\Contracts\Love\Reactant\ReactionCounter\Exceptions\ReactionCounterDuplicate;
20
use Cog\Contracts\Love\Reactant\ReactionCounter\Models\ReactionCounter as ReactionCounterInterface;
21
use Cog\Contracts\Love\Reactant\ReactionTotal\Exceptions\ReactionTotalDuplicate;
22
use Cog\Contracts\Love\Reactant\ReactionTotal\Models\ReactionTotal as ReactionTotalInterface;
23
use Cog\Contracts\Love\Reacter\Models\Reacter;
24
use Cog\Contracts\Love\Reacter\Models\Reacter as ReacterInterface;
25
use Cog\Contracts\Love\Reaction\Models\Reaction as ReactionInterface;
26
use Cog\Contracts\Love\ReactionType\Models\ReactionType as ReactionTypeInterface;
27
use Cog\Laravel\Love\Reactant\ReactionCounter\Models\NullReactionCounter;
28
use Cog\Laravel\Love\Reactant\ReactionCounter\Models\ReactionCounter;
29
use Cog\Laravel\Love\Reactant\ReactionTotal\Models\NullReactionTotal;
30
use Cog\Laravel\Love\Reactant\ReactionTotal\Models\ReactionTotal;
31
use Cog\Laravel\Love\Reaction\Models\Reaction;
32
use Cog\Laravel\Love\Support\Database\Eloquent\Model;
33
use Illuminate\Database\Eloquent\Casts\Attribute;
34
use Illuminate\Database\Eloquent\Factories\HasFactory;
35
use Illuminate\Database\Eloquent\Relations\HasMany;
36
use Illuminate\Database\Eloquent\Relations\HasOne;
37
use Illuminate\Database\Eloquent\Relations\MorphTo;
38
use Illuminate\Support\Collection;
39
40
final class Reactant extends Model implements
41
    ReactantInterface
42
{
43
    use HasFactory;
44
45
    protected $table = 'love_reactants';
46
47
    protected static $unguarded = true;
48
49
    public function id(): Attribute
50
    {
51
        return new Attribute(
52
            get: fn (string | null $value) => $value,
53
            set: fn (string | null $value) => $value,
54
        );
55
    }
56
57
    public function reactable(): MorphTo
58
    {
59
        return $this->morphTo('reactable', 'type', 'id', 'love_reactant_id');
60
    }
61
62
    public function reactions(): HasMany
63
    {
64
        return $this->hasMany(Reaction::class, 'reactant_id');
65
    }
66
67
    public function reactionCounters(): HasMany
68
    {
69
        return $this->hasMany(ReactionCounter::class, 'reactant_id');
70
    }
71
72
    public function reactionTotal(): HasOne
73
    {
74
        return $this->hasOne(ReactionTotal::class, 'reactant_id');
75
    }
76
77
    public function getId(): string
78
    {
79
        return $this->getAttributeValue('id');
80
    }
81
82
    public function getReactable(): ReactableInterface
83
    {
84
        $reactable = $this->getAttribute('reactable');
85
86
        if ($reactable === null) {
87
            throw new NotAssignedToReactable();
88
        }
89
90
        return $reactable;
91
    }
92
93
    public function getReactions(): iterable
94
    {
95
        return $this->getAttribute('reactions');
96
    }
97
98
    /**
99
     * @return iterable|\Cog\Contracts\Love\Reaction\Models\Reaction[]
100
     */
101
    public function getReactionsBy(
102
        Reacter $reacter,
103
    ): iterable {
104
        if ($reacter->isNull()) {
105
            return new Collection();
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Illuminate\Support\Collection() returns the type Illuminate\Support\Collection which is incompatible with the documented return type Cog\Contracts\Love\React...els\Reaction[]|iterable.
Loading history...
106
        }
107
108
        // TODO: Test if relation was loaded partially
109
        if ($this->relationLoaded('reactions')) {
110
            return $this
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getAttribu...ion(...) { /* ... */ }) returns the type boolean which is incompatible with the type-hinted return iterable.
Loading history...
111
                ->getAttribute('reactions')
112
                ->contains(fn (ReactionInterface $reaction) => $reaction->isByReacter($reacter));
113
        }
114
115
        return $this->reactions()->where('reacter_id', $reacter->getId())->get();
116
    }
117
118
    public function getReactionCounters(): iterable
119
    {
120
        return $this->getAttribute('reactionCounters');
121
    }
122
123
    public function getReactionCounterOfType(
124
        ReactionTypeInterface $reactionType,
125
    ): ReactionCounterInterface {
126
        // TODO: Test query count with eager loaded relation
127
        // TODO: Test query count without eager loaded relation
128
        $counter = $this
129
            ->getAttribute('reactionCounters')
130
            ->where('reaction_type_id', $reactionType->getId())
131
            ->first();
132
133
        if ($counter === null) {
134
            return new NullReactionCounter($this, $reactionType);
135
        }
136
137
        return $counter;
138
    }
139
140
    public function getReactionTotal(): ReactionTotalInterface
141
    {
142
        return $this->getAttribute('reactionTotal')
143
            ?? new NullReactionTotal($this);
144
    }
145
146
    public function isReactedBy(
147
        ReacterInterface $reacter,
148
        ReactionTypeInterface | null $reactionType = null,
149
        float | null $rate = null,
150
    ): bool {
151
        if ($reacter->isNull()) {
152
            return false;
153
        }
154
155
        // TODO: Test if relation was loaded partially
156
        if ($this->relationLoaded('reactions')) {
157
            return $this
158
                ->getAttribute('reactions')
159
                ->contains(function (ReactionInterface $reaction) use ($reacter, $reactionType, $rate) {
160
                    if ($reaction->isNotByReacter($reacter)) {
161
                        return false;
162
                    }
163
164
                    if ($reactionType !== null && $reaction->isNotOfType($reactionType)) {
165
                        return false;
166
                    }
167
168
                    if ($rate !== null && $reaction->getRate() !== $rate) {
169
                        return false;
170
                    }
171
172
                    return true;
173
                });
174
        }
175
176
        $query = $this->reactions()->where('reacter_id', $reacter->getId());
177
178
        if ($reactionType !== null) {
179
            $query->where('reaction_type_id', $reactionType->getId());
180
        }
181
182
        if ($rate !== null) {
183
            $query->where('rate', $rate);
184
        }
185
186
        return $query->exists();
187
    }
188
189
    public function isNotReactedBy(
190
        ReacterInterface $reacter,
191
        ReactionTypeInterface | null $reactionType = null,
192
        float | null $rate = null,
193
    ): bool {
194
        return !$this->isReactedBy($reacter, $reactionType, $rate);
195
    }
196
197
    public function isEqualTo(
198
        ReactantInterface $that,
199
    ): bool {
200
        return $that->isNotNull()
201
            && $this->getId() === $that->getId();
202
    }
203
204
    public function isNotEqualTo(
205
        ReactantInterface $that,
206
    ): bool {
207
        return !$this->isEqualTo($that);
208
    }
209
210
    public function isNull(): bool
211
    {
212
        return !$this->exists;
213
    }
214
215
    public function isNotNull(): bool
216
    {
217
        return $this->exists;
218
    }
219
220
    public function createReactionCounterOfType(
221
        ReactionTypeInterface $reactionType,
222
    ): void {
223
        if ($this->reactionCounters()->where('reaction_type_id', $reactionType->getId())->exists()) {
224
            throw ReactionCounterDuplicate::ofTypeForReactant($reactionType, $this);
225
        }
226
227
        $this->reactionCounters()->create([
228
            'reaction_type_id' => $reactionType->getId(),
229
        ]);
230
231
        // Need to reload relation with fresh data
232
        $this->load('reactionCounters');
233
    }
234
235
    public function createReactionTotal(): void
236
    {
237
        if ($this->reactionTotal()->exists()) {
238
            throw ReactionTotalDuplicate::forReactant($this);
239
        }
240
241
        $this->reactionTotal()->create();
242
243
        // Need to reload relation with fresh data
244
        $this->load('reactionTotal');
245
    }
246
}
247