Issues (25)

src/TranslatableScope.php (3 issues)

1
<?php
2
3
namespace Laraplus\Data;
4
5
use Illuminate\Support\Str;
6
use Illuminate\Database\Eloquent\Scope;
7
use Illuminate\Database\Query\Expression;
8
use Illuminate\Database\Query\JoinClause;
9
use Illuminate\Database\Query\Grammars\Grammar;
10
use Illuminate\Database\Eloquent\Model as Eloquent;
11
use Illuminate\Database\Query\Grammars\SqlServerGrammar;
12
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
13
14
class TranslatableScope implements Scope
15
{
16
    protected $table;
17
18
    protected $i18nTable;
19
20
    protected $locale;
21
22
    protected $fallback;
23
24
    protected $joinType = 'leftJoin';
25
26
    /**
27
     * Apply the scope to a given Eloquent query builder.
28
     *
29
     * @param EloquentBuilder $builder
30
     * @param Eloquent $model
31
     *
32
     * @return void
33
     */
34
    public function apply(EloquentBuilder $builder, Eloquent $model)
35
    {
36
        $this->table = $model->getTable();
37
        $this->locale = $model->getLocale();
38
        $this->i18nTable = $model->getI18nTable();
39
        $this->fallback = $model->getFallbackLocale();
40
41
        if (!Str::startsWith($this->table, 'laravel_reserved_')) {
42
            $this->createJoin($builder, $model);
43
            $this->createWhere($builder, $model);
44
            $this->createSelect($builder, $model);
45
        }
46
    }
47
48
    /**
49
     * Create the join clause.
50
     *
51
     * @param EloquentBuilder $builder
52
     * @param Eloquent $model
53
     */
54
    protected function createJoin(EloquentBuilder $builder, Eloquent $model)
55
    {
56
        $joinType = $this->getJoinType($model);
57
        $clause = $this->getJoinClause($model, $this->locale, $this->i18nTable);
58
59
        $builder->$joinType($this->i18nTable, $clause);
60
61
        if ($model->shouldFallback()) {
62
            $clause = $this->getJoinClause($model, $this->fallback, $this->i18nTable.'_fallback');
63
64
            $builder->$joinType("{$this->i18nTable} as {$this->i18nTable}_fallback", $clause);
65
        }
66
    }
67
68
    /**
69
     * Return the join type.
70
     *
71
     * @param Eloquent $model
72
     *
73
     * @return string
74
     */
75
    protected function getJoinType(Eloquent $model)
76
    {
77
        $innerJoin = !$model->shouldFallback() && $model->getOnlyTranslated();
78
79
        return $innerJoin ? 'join' : 'leftJoin';
80
    }
81
82
    /**
83
     * Return the join clause.
84
     *
85
     * @param Eloquent $model
86
     * @param string $locale
87
     * @param string $alias
88
     *
89
     * @return callable
90
     */
91
    protected function getJoinClause(Eloquent $model, $locale, $alias)
92
    {
93
        return function (JoinClause $join) use ($model, $locale, $alias) {
94
            $primary = $model->getKeyName();
95
            $foreign = $model->getForeignKey();
96
            $langKey = $model->getLocaleKey();
97
98
            $join->on($alias.'.'.$foreign, '=', $this->table.'.'.$primary)
99
                ->where($alias.'.'.$langKey, '=', $locale);
0 ignored issues
show
Are you sure $langKey of type Illuminate\Database\Eloquent\Builder|mixed can be used in concatenation? ( Ignorable by Annotation )

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

99
                ->where($alias.'.'./** @scrutinizer ignore-type */ $langKey, '=', $locale);
Loading history...
100
        };
101
    }
102
103
    /**
104
     * Create the where clause.
105
     *
106
     * @param EloquentBuilder $builder
107
     * @param Eloquent $model
108
     */
109
    protected function createWhere(EloquentBuilder $builder, Eloquent $model)
110
    {
111
        if ($model->getOnlyTranslated() && $model->shouldFallback()) {
112
            $key = $model->getForeignKey();
113
            $primary = "{$this->i18nTable}.{$key}";
114
            $fallback = "{$this->i18nTable}_fallback.{$key}";
115
            $ifNull = $builder->getQuery()->compileIfNull($primary, $fallback);
116
117
            $builder->whereRaw("$ifNull is not null");
118
        }
119
    }
120
121
    /**
122
     * Create the select clause.
123
     *
124
     * @param EloquentBuilder $builder
125
     * @param Eloquent $model
126
     */
127
    protected function createSelect(EloquentBuilder $builder, Eloquent $model)
128
    {
129
        if ($builder->getQuery()->columns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $builder->getQuery()->columns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
130
            return;
131
        }
132
133
        $select = $this->formatColumns($builder, $model);
134
135
        $builder->select(array_merge([$this->table.'.*'], $select));
136
    }
137
138
    /**
139
     * Format the columns.
140
     *
141
     * @param EloquentBuilder $builder
142
     * @param Eloquent $model
143
     *
144
     * @return array
145
     */
146
    protected function formatColumns(EloquentBuilder $builder, Eloquent $model)
147
    {
148
        $map = function ($field) use ($builder, $model) {
149
            if (!$model->shouldFallback()) {
150
                return "{$this->i18nTable}.{$field}";
151
            }
152
153
            $primary = "{$this->i18nTable}.{$field}";
154
            $fallback = "{$this->i18nTable}_fallback.{$field}";
155
            $alias = $field;
156
157
            return new Expression($builder->getQuery()->compileIfNull($primary, $fallback, $alias));
158
        };
159
160
        return array_map($map, $model->translatableAttributes());
0 ignored issues
show
It seems like $model->translatableAttributes() can also be of type Illuminate\Database\Eloquent\Builder; however, parameter $array of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

160
        return array_map($map, /** @scrutinizer ignore-type */ $model->translatableAttributes());
Loading history...
161
    }
162
163
    /**
164
     * Return string based on null type.
165
     *
166
     * @param Grammar $grammar
167
     *
168
     * @return string
169
     */
170
    protected function getIfNull(Grammar $grammar)
171
    {
172
        return $grammar instanceof SqlServerGrammar ? 'isnull' : 'ifnull';
173
    }
174
175
    /**
176
     * Extend the builder.
177
     *
178
     * @param EloquentBuilder $builder
179
     */
180
    public function extend(EloquentBuilder $builder)
181
    {
182
        $builder->macro('onlyTranslated', function (EloquentBuilder $builder, $locale = null) {
183
            $builder->getModel()->setOnlyTranslated(true);
184
185
            if ($locale) {
186
                $builder->getModel()->setLocale($locale);
187
            }
188
189
            return $builder;
190
        });
191
192
        $builder->macro('withUntranslated', function (EloquentBuilder $builder) {
193
            $builder->getModel()->setOnlyTranslated(false);
194
195
            return $builder;
196
        });
197
198
        $builder->macro('withFallback', function (EloquentBuilder $builder, $fallbackLocale = null) {
199
            $builder->getModel()->setWithFallback(true);
200
201
            if ($fallbackLocale) {
202
                $builder->getModel()->setFallbackLocale($fallbackLocale);
203
            }
204
205
            return $builder;
206
        });
207
208
        $builder->macro('withoutFallback', function (EloquentBuilder $builder) {
209
            $builder->getModel()->setWithFallback(false);
210
211
            return $builder;
212
        });
213
214
        $builder->macro('translateInto', function (EloquentBuilder $builder, $locale) {
215
            if ($locale) {
216
                $builder->getModel()->setLocale($locale);
217
            }
218
219
            return $builder;
220
        });
221
222
        $builder->macro('withoutTranslations', function (EloquentBuilder $builder) {
223
            $builder->withoutGlobalScope(static::class);
224
225
            return $builder;
226
        });
227
228
        $builder->macro('withAllTranslations', function (EloquentBuilder $builder) {
229
            $builder->withoutGlobalScope(static::class)->with('translations');
230
231
            return $builder;
232
        });
233
    }
234
}
235