Passed
Push — develop ( d01060...4713f8 )
by Paul
14:54
created

Review::offsetSet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 10
ccs 0
cts 7
cp 0
rs 10
cc 2
nc 2
nop 2
crap 6
1
<?php
2
3
namespace GeminiLabs\SiteReviews;
4
5
use GeminiLabs\SiteReviews\Database\OptionManager;
6
use GeminiLabs\SiteReviews\Database\PostMeta;
7
use GeminiLabs\SiteReviews\Database\Query;
8
use GeminiLabs\SiteReviews\Database\ReviewManager;
9
use GeminiLabs\SiteReviews\Defaults\CustomFieldsDefaults;
10
use GeminiLabs\SiteReviews\Defaults\ReviewDefaults;
11
use GeminiLabs\SiteReviews\Defaults\StatDefaults;
12
use GeminiLabs\SiteReviews\Helpers\Arr;
13
use GeminiLabs\SiteReviews\Helpers\Cast;
14
use GeminiLabs\SiteReviews\Helpers\Text;
15
use GeminiLabs\SiteReviews\Modules\Avatar;
16
use GeminiLabs\SiteReviews\Modules\Date;
17
use GeminiLabs\SiteReviews\Modules\Encryption;
18
use GeminiLabs\SiteReviews\Modules\Html\ReviewHtml;
19
use GeminiLabs\SiteReviews\Modules\Multilingual;
20
use GeminiLabs\SiteReviews\Modules\Sanitizer;
21
22
/**
23
 * @property bool      $approved       This property is mapped to $is_approved
24
 * @property array     $assigned_posts
25
 * @property array     $assigned_terms
26
 * @property array     $assigned_users
27
 * @property string    $author
28
 * @property int       $author_id
29
 * @property string    $avatar
30
 * @property string    $content
31
 * @property Arguments $custom
32
 * @property string    $date
33
 * @property string    $date_gmt
34
 * @property string    $name           This property is mapped to $author
35
 * @property string    $email
36
 * @property bool      $has_revisions  This property is mapped to $is_modified
37
 * @property int       $ID
38
 * @property string    $ip_address
39
 * @property bool      $is_approved
40
 * @property bool      $is_modified
41
 * @property bool      $is_pinned
42
 * @property bool      $is_verified
43
 * @property bool      $modified       This property is mapped to $is_modified
44
 * @property bool      $pinned         This property is mapped to $is_pinned
45
 * @property int       $rating
46
 * @property int       $rating_id
47
 * @property string    $response
48
 * @property int       $score
49
 * @property string    $status
50
 * @property bool      $terms
51
 * @property string    $title
52
 * @property string    $type
53
 * @property string    $url
54
 * @property int       $user_id        This property is mapped to $author_id
55
 */
56
class Review extends Arguments
57
{
58
    /** @var Arguments|null */
59
    protected $_meta;
60
61
    /** @var \WP_Post|null */
62
    protected $_post;
63
64
    protected bool $has_checked_revisions = false;
65
66
    /**
67
     * @param array|object $values
68
     */
69 18
    public function __construct($values = [], bool $init = true)
70
    {
71 18
        $args = glsr(ReviewDefaults::class)->restrict( // maps ID to rating_id, then review_id to ID
72 18
            glsr()->args($values)->toArray()
73 18
        );
74 18
        parent::__construct($args);
75 18
        if (!$init || empty($values)) {
76 18
            return;
77
        }
78 18
        if (empty($this->author)) {
79
            $this->set('author', glsr(Sanitizer::class)->sanitizeUserName(
80
                $this->user(), __('Anonymous', 'site-reviews')
81
            ));
82
        }
83 18
        $this->set('avatar', glsr(Avatar::class)->url($this));
84 18
        $this->set('custom', $this->custom());
85 18
        $this->set('response', $this->meta()->_response);
86
    }
87
88
    /**
89
     * @return mixed
90
     */
91
    public function __call(string $method, array $args)
92
    {
93
        array_unshift($args, $this);
94
        $result = apply_filters_ref_array(glsr()->id."/review/call/{$method}", $args);
95
        if (!is_a($result, get_class($this))) {
96
            return $result;
97
        }
98
    }
99
100
    public function __toString(): string
101
    {
102
        return (string) $this->build();
103
    }
104
105
    public function approveUrl(): string
106
    {
107
        $token = glsr(Encryption::class)->encryptRequest('approve', [$this->ID]);
108
        return !empty($token)
109
            ? add_query_arg(glsr()->prefix, $token, admin_url())
110
            : '';
111
    }
112
113
    public function assignedPosts(bool $multilingual = true): array
114
    {
115
        $postIds = $this->assigned_posts;
116
        if ($multilingual) {
117
            $postIds = glsr(Multilingual::class)->getPostIds($postIds);
118
        }
119
        if (empty($postIds)) {
120
            return [];
121
        }
122
        return get_posts([
123
            'post__in' => $postIds,
124
            'post_type' => 'any',
125
            'posts_per_page' => -1,
126
        ]);
127
    }
128
129
    public function assignedTerms(bool $multilingual = true): array
130
    {
131
        $termIds = $this->assigned_terms;
132
        if ($multilingual) {
133
            $termIds = glsr(Multilingual::class)->getTermIds($termIds);
134
        }
135
        if (empty($termIds)) {
136
            return [];
137
        }
138
        $terms = get_terms([
139
            'hide_empty' => !$multilingual,
140
            'include' => $termIds,
141
            'taxonomy' => glsr()->taxonomy,
142
        ]);
143
        if (is_wp_error($terms)) {
144
            return [];
145
        }
146
        return $terms;
147
    }
148
149
    public function assignedUsers(): array
150
    {
151
        if (empty($this->assigned_users)) {
152
            return [];
153
        }
154
        return get_users([
155
            'fields' => ['display_name', 'ID', 'user_email', 'user_nicename', 'user_url'],
156
            'include' => $this->assigned_users,
157
        ]);
158
    }
159
160
    public function author(): string
161
    {
162
        return Text::name(
163
            $this->author,
164
            glsr_get_option('reviews.name.format', '', 'string'),
165
            glsr_get_option('reviews.name.initial', '', 'string')
166
        );
167
    }
168
169
    public function avatar(int $size = 0): string
170
    {
171
        return glsr(Avatar::class)->img($this, $size);
172
    }
173
174
    public function build(array $args = []): ReviewHtml
175
    {
176
        return new ReviewHtml($this, $args);
177
    }
178
179 18
    public function custom(): Arguments
180
    {
181 18
        $custom = array_filter($this->meta()->toArray(),
182 18
            fn ($key) => str_starts_with($key, '_custom'),
183 18
            ARRAY_FILTER_USE_KEY
184 18
        );
185 18
        $custom = Arr::unprefixKeys($custom, '_custom_');
186 18
        $custom = Arr::unprefixKeys($custom, '_');
187 18
        $custom = glsr(CustomFieldsDefaults::class)->merge($custom);
188 18
        return glsr()->args($custom);
189
    }
190
191
    public function date(string $format = 'F j, Y'): string
192
    {
193
        if (!empty(func_get_args())) {
194
            return Cast::toString(mysql2date($format, $this->date));
195
        }
196
        $dateFormat = glsr_get_option('reviews.date.format', 'default');
197
        if ('relative' === $dateFormat) {
198
            return glsr(Date::class)->relative($this->date, 'past');
199
        }
200
        $format = 'custom' === $dateFormat
201
            ? glsr_get_option('reviews.date.custom', 'M j, Y')
202
            : glsr(OptionManager::class)->wp('date_format', 'F j, Y');
203
        return Cast::toString(mysql2date($format, $this->date));
204
    }
205
206
    public function editUrl(): string
207
    {
208
        $obj = get_post_type_object(glsr()->post_type);
209
        $link = admin_url(sprintf($obj->_edit_link.'&action=edit', $this->ID));
210
        $link = apply_filters('get_edit_post_link', $link, $this->ID, 'display');
211
        return Cast::toString($link);
212
    }
213
214
    /** @param int|\WP_Post $post */
215
    public static function isEditable($post): bool
216
    {
217
        $postId = Helper::getPostId($post);
218
        return static::isReview($postId)
219
            && in_array(glsr_get_review($postId)->type, ['', 'local']);
220
    }
221
222
    /** @param \WP_Post|int|false $post */
223 22
    public static function isReview($post): bool
224
    {
225 22
        return glsr()->post_type === get_post_type($post);
226
    }
227
228 18
    public function isValid(): bool
229
    {
230 18
        return !empty($this->ID) && !empty($this->rating_id);
231
    }
232
233
    public function location(): array
234
    {
235
        return glsr(StatDefaults::class)->restrict(
236
            glsr(PostMeta::class)->get($this->ID, 'geolocation')
237
        );
238
    }
239
240 18
    public function meta(): Arguments
241
    {
242 18
        if (!$this->_meta instanceof Arguments) {
243 18
            $meta = Arr::consolidate(get_post_meta($this->ID));
244 18
            $meta = array_map(fn ($item) => array_shift($item), array_filter($meta));
245 18
            $meta = array_filter($meta, '\GeminiLabs\SiteReviews\Helper::isNotEmpty');
246 18
            $meta = array_map('maybe_unserialize', $meta);
247 18
            $this->_meta = glsr()->args($meta);
248
        }
249 18
        return $this->_meta;
250
    }
251
252
    /**
253
     * @param mixed $key
254
     */
255 18
    #[\ReturnTypeWillChange]
256
    public function offsetExists($key): bool
257
    {
258 18
        return parent::offsetExists($key) || !is_null($this->custom()->$key);
259
    }
260
261
    /**
262
     * @param mixed $key
263
     *
264
     * @return mixed
265
     */
266 18
    #[\ReturnTypeWillChange]
267
    public function offsetGet($key)
268
    {
269 18
        $alternateKeys = [
270 18
            'approved' => 'is_approved',
271 18
            'has_revisions' => 'is_modified',
272 18
            'modified' => 'is_modified',
273 18
            'name' => 'author',
274 18
            'pinned' => 'is_pinned',
275 18
            'user_id' => 'author_id',
276 18
        ];
277 18
        if (array_key_exists($key, $alternateKeys)) {
278
            return $this->offsetGet($alternateKeys[$key]);
279
        }
280 18
        if ('is_modified' === $key) {
281
            return $this->hasRevisions();
282
        }
283 18
        if (is_null($value = parent::offsetGet($key))) {
284
            return $this->custom()->$key;
285
        }
286 18
        return $value;
287
    }
288
289
    /**
290
     * @param mixed $key
291
     */
292
    #[\ReturnTypeWillChange]
293
    public function offsetSet($key, $value): void
294
    {
295
        // This class is read-only, except for custom fields
296
        if ('custom' === $key) {
297
            $value = Arr::consolidate($value);
298
            $value = Arr::prefixKeys($value, '_custom_');
299
            $meta = wp_parse_args($value, $this->meta()->toArray());
300
            $this->_meta = glsr()->args($meta);
301
            parent::offsetSet($key, $this->custom());
302
        }
303
    }
304
305
    /**
306
     * @param mixed $key
307
     */
308
    #[\ReturnTypeWillChange]
309
    public function offsetUnset($key): void
310
    {
311
        // This class is read-only
312
    }
313
314
    /**
315
     * @return \WP_Post|null
316
     */
317
    public function post()
318
    {
319
        if (!$this->_post instanceof \WP_Post) {
320
            $this->_post = get_post($this->ID);
321
        }
322
        return $this->_post;
323
    }
324
325
    public function rating(): string
326
    {
327
        return glsr_star_rating($this->rating);
328
    }
329
330 18
    public function refresh(): Review
331
    {
332 18
        $values = glsr(ReviewManager::class)->get($this->ID, true)->toArray();
333 18
        $this->merge($values);
334 18
        $this->_meta = null;
335 18
        $this->_post = null;
336 18
        $this->has_checked_revisions = false;
337 18
        $this->set('avatar', glsr(Avatar::class)->url($this));
338 18
        $this->set('custom', $this->custom());
339 18
        $this->set('response', $this->meta()->_response);
340 18
        return $this;
341
    }
342
343
    public function render(array $args = []): void
344
    {
345
        echo $this->build($args);
346
    }
347
348 18
    public function toArray(array $excludedKeys = []): array
349
    {
350 18
        $excludedKeys = Arr::consolidate($excludedKeys);
351 18
        $values = Cast::toArrayDeep($this->getArrayCopy());
352 18
        $values['name'] = $this->author; // fallback
353 18
        return array_diff_key($values, array_flip($excludedKeys));
354
    }
355
356
    public function type(): string
357
    {
358
        $type = $this->type;
359
        $reviewTypes = glsr()->retrieveAs('array', 'review_types');
360
        return Arr::get($reviewTypes, $type, _x('Unknown', 'admin-text', 'site-reviews'));
361
    }
362
363
    /**
364
     * @return \WP_User|false
365
     */
366
    public function user()
367
    {
368
        return get_user_by('id', $this->author_id);
369
    }
370
371 18
    public function verifyUrl(string $path = ''): string
372
    {
373 18
        if ($this->is_verified && !empty($this->meta()->_verified_on)) {
374
            return '';
375
        }
376 18
        $path = trailingslashit($path);
377 18
        $token = glsr(Encryption::class)->encryptRequest('verify', [$this->ID, $path]);
378 18
        return !empty($token)
379 18
            ? add_query_arg(glsr()->prefix, $token, get_home_url())
380 18
            : '';
381
    }
382
383
    protected function hasRevisions(): bool
384
    {
385
        if (!$this->has_checked_revisions) {
386
            $modified = glsr(Query::class)->hasRevisions($this->ID);
387
            $this->set('is_modified', $modified);
388
            $this->has_checked_revisions = true;
389
        }
390
        return $this->is_modified;
391
    }
392
}
393