Issues (426)

src/Commands/RefreshCrops.php (4 issues)

1
<?php
2
3
namespace A17\Twill\Commands;
4
5
use Carbon\Carbon;
6
use Illuminate\Database\DatabaseManager;
7
use Illuminate\Database\Query\Builder;
8
use Illuminate\Support\Collection;
9
use Illuminate\Support\Str;
10
use A17\Twill\Models\Media;
11
12
class RefreshCrops extends Command
13
{
14
    /**
15
     * The name and signature of the console command.
16
     *
17
     * @var string
18
     */
19
    protected $signature = 'twill:refresh-crops
20
        {modelName : The fully qualified model name (e.g. App\Models\Post)}
21
        {roleName : The role name for which crops will be refreshed}
22
        {--dry : Print the operations that would be performed without modifying the database}
23
    ';
24
25
    /**
26
     * The console command description.
27
     *
28
     * @var string
29
     */
30
    protected $description = 'Refresh all crops for an existing image role';
31
32
    /**
33
     * @var DatabaseManager
34
     */
35
    protected $db;
36
37
    /**
38
     * The model FQCN for this operation.
39
     *
40
     * @var string
41
     */
42
    protected $modelName;
43
44
    /**
45
     * The role name for this operation.
46
     *
47
     * @var string
48
     */
49
    protected $roleName;
50
51
    /**
52
     * Available crops for the given model and role name.
53
     *
54
     * @var Collection
55
     */
56
    protected $crops;
57
58
    /**
59
     * Print the operations that would be performed without modifying the database.
60
     *
61
     * @var bool
62
     */
63
    protected $isDryRun = false;
64
65
    /**
66
     * Total number of crops created.
67
     *
68
     * @var int
69
     */
70
    protected $cropsCreated = 0;
71
72
    /**
73
     * Total number of crops deleted.
74
     *
75
     * @var int
76
     */
77
    protected $cropsDeleted = 0;
78
79
    /**
80
     * Cache for Media models queried during this operation.
81
     *
82
     * @var array
83
     */
84
    protected $mediaCache = [];
85
86
    /**
87
     * @param DatabaseManager $db
88
     */
89
    public function __construct(DatabaseManager $db)
90
    {
91
        parent::__construct();
92
93
        $this->db = $db;
94
    }
95
96
    public function handle()
97
    {
98
        $this->isDryRun = $this->option('dry');
0 ignored issues
show
Documentation Bug introduced by
The property $isDryRun was declared of type boolean, but $this->option('dry') is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
99
100
        $this->modelName = $this->locateModel($this->argument('modelName'));
0 ignored issues
show
It seems like $this->argument('modelName') can also be of type array; however, parameter $modelName of A17\Twill\Commands\RefreshCrops::locateModel() does only seem to accept string, 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

100
        $this->modelName = $this->locateModel(/** @scrutinizer ignore-type */ $this->argument('modelName'));
Loading history...
101
102
        if (! $this->modelName) {
103
            $this->error("Model `{$this->argument('modelName')}` was not found`");
104
105
            return 1;
106
        }
107
108
        $this->roleName = $this->argument('roleName');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->argument('roleName') can also be of type array. However, the property $roleName is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
109
110
        $mediasParams = app($this->modelName)->mediasParams;
111
112
        if (! isset($mediasParams[$this->roleName])) {
113
            $this->error("Role `{$this->roleName}` was not found`");
114
115
            return 1;
116
        }
117
118
        $this->crops = collect($mediasParams[$this->roleName]);
119
120
        $mediables = $this->db
121
            ->table(config('twill.mediables_table', 'twill_mediables'))
122
            ->where(['mediable_type' => $this->modelName, 'role' => $this->roleName]);
123
124
        if ($mediables->count() === 0) {
125
            $this->warn("No mediables found for model `$this->modelName` and role `$this->roleName`");
126
127
            return 1;
128
        }
129
130
        if ($this->isDryRun) {
131
            $this->warn("**Dry Run** No changes are being made to the database");
132
            $this->warn("");
133
        }
134
135
        $this->processMediables($mediables);
136
137
        $this->printSummary();
138
    }
139
140
    /**
141
     * Print a summary of all crops created and deleted at the end of the command.
142
     *
143
     * @return void
144
     */
145
    protected function printSummary()
146
    {
147
        if ($this->cropsCreated + $this->cropsDeleted === 0) {
148
            $this->info("");
149
            $this->info("No crops to create or delete for this model and role");
150
            return;
151
        }
152
153
        $this->info("");
154
        $this->info("Summary:");
155
156
        $actionPrefix = $this->isDryRun ? 'to be ' : '';
157
158
        if ($this->cropsCreated > 0) {
159
            $noun = Str::plural('crop', $this->cropsCreated);
160
            $this->info("{$this->cropsCreated} {$noun} {$actionPrefix}created");
161
        }
162
163
        if ($this->cropsDeleted > 0) {
164
            $noun = Str::plural('crop', $this->cropsDeleted);
165
            $this->info("{$this->cropsDeleted} {$noun} {$actionPrefix}deleted");
166
        }
167
    }
168
169
    /**
170
     * Process a set of mediable items.
171
     *
172
     * @param Builder $mediables
173
     * @return void
174
     */
175
    protected function processMediables(Builder $mediables)
176
    {
177
        // Handle locales separately because not all items have a 1-1 match in other locales
178
        foreach ($mediables->get()->groupBy('locale') as $locale => $itemsByLocale) {
179
180
            // Group items by mediable_id to get related crops
181
            foreach ($itemsByLocale->groupBy('mediable_id') as $mediableId => $itemsByMediableId) {
182
183
                // Then, group by media_id to handle slideshows (multiple entries for one role)
184
                foreach ($itemsByMediableId->groupBy('media_id') as $mediaId => $items) {
185
                    $existingCrops = $items->keyBy('crop')->keys();
186
                    $allCrops = $this->crops->keys();
187
188
                    if ($cropsToCreate = $allCrops->diff($existingCrops)->all()) {
189
                        $this->createCrops($cropsToCreate, $items[0]);
190
                    }
191
192
                    if ($cropsToDelete = $existingCrops->diff($allCrops)->all()) {
193
                        $this->deleteCrops($cropsToDelete, $items[0]);
194
                    }
195
                }
196
            }
197
        }
198
    }
199
200
    /**
201
     * Create missing crops for a given mediable item, preserving existing metadata.
202
     *
203
     * @param string[] $crops List of crop names to create.
204
     * @param object $baseItem Base mediable object from which to pull information.
205
     * @return void
206
     */
207
    protected function createCrops($crops, $baseItem)
208
    {
209
        $this->cropsCreated += count($crops);
210
211
        if ($this->isDryRun) {
212
            $cropNames = collect($crops)->join(', ');
213
            $noun = Str::plural('crop', count($crops));
214
            $this->info("Create {$noun} `{$cropNames}` for mediable_id=`{$baseItem->mediable_id}` and media_id=`{$baseItem->media_id}`");
215
            return;
216
        }
217
218
        foreach ($crops as $crop) {
219
            $ratio = $this->crops[$crop][0];
220
            $cropParams = $this->getCropParams($baseItem->media_id, $ratio['ratio']);
221
222
            $this->db
223
                ->table(config('twill.mediables_table', 'twill_mediables'))
224
                ->insert([
225
                    'created_at' => Carbon::now(),
226
                    'updated_at' => Carbon::now(),
227
                    'mediable_id' => $baseItem->mediable_id,
228
                    'mediable_type' => $baseItem->mediable_type,
229
                    'media_id' => $baseItem->media_id,
230
                    'role' => $baseItem->role,
231
                    'crop' => $crop,
232
                    'lqip_data' => null,
233
                    'ratio' => $ratio['name'],
234
                    'metadatas' => $baseItem->metadatas,
235
                    'locale' => $baseItem->locale,
236
                ] + $cropParams);
237
        }
238
    }
239
240
    /**
241
     * Delete unused crops for a given mediable item.
242
     *
243
     * @param string[] $crops List of crop names to create.
244
     * @param object $baseItem Base mediable object from which to pull information.
245
     * @return void
246
     */
247
    protected function deleteCrops($crops, $baseItem)
248
    {
249
        $this->cropsDeleted += count($crops);
250
251
        if ($this->isDryRun) {
252
            $cropNames = collect($crops)->join(', ');
253
            $noun = Str::plural('crop', count($crops));
254
            $this->info("Delete {$noun} `$cropNames` for mediable_id=`$baseItem->mediable_id` and media_id=`$baseItem->media_id`");
255
            return;
256
        }
257
258
        $this->db
259
            ->table(config('twill.mediables_table', 'twill_mediables'))
260
            ->where([
261
                'mediable_type' => $baseItem->mediable_type,
262
                'mediable_id' => $baseItem->mediable_id,
263
                'media_id' => $baseItem->media_id,
264
                'role' => $baseItem->role,
265
            ])
266
            ->whereIn('crop', $crops)
267
            ->delete();
268
    }
269
270
    /**
271
     * Attempt to locate the model from the given command argument.
272
     *
273
     * @param string $modelName
274
     * @return string|null  The model FQCN.
275
     */
276
    protected function locateModel($modelName)
277
    {
278
        $modelName = ltrim($modelName, "\\");
279
        $modelStudly = Str::studly($modelName);
280
        $moduleName = Str::plural($modelStudly);
281
        $namespace = config('twill.namespace', 'App');
282
283
        $attempts = [
284
            $modelName,
285
            "$namespace\\Models\\$modelStudly",
286
            "$namespace\\Twill\\Capsules\\$moduleName\\Models\\$modelStudly",
287
        ];
288
289
        foreach ($attempts as $phpClass) {
290
            if (class_exists($phpClass)) {
291
                return $phpClass;
292
            }
293
        }
294
295
        return null;
296
    }
297
298
    /**
299
     * Calculate crop params for a media from a given ratio.
300
     *
301
     * @param int $mediaId
302
     * @param float $ratio
303
     * @return array
304
     */
305
    protected function getCropParams($mediaId, $ratio)
306
    {
307
        if (!isset($this->mediaCache[$mediaId])) {
308
            $this->mediaCache[$mediaId] = Media::find($mediaId);
309
        }
310
311
        $width = $this->mediaCache[$mediaId]->width;
312
        $height = $this->mediaCache[$mediaId]->height;
313
        $originalRatio = $width / $height;
314
315
        if ($ratio === 0) {
0 ignored issues
show
The condition $ratio === 0 is always false.
Loading history...
316
            $crop_w = $width;
317
            $crop_h = $height;
318
            $crop_x = 0;
319
            $crop_y = 0;
320
        } elseif ($originalRatio <= $ratio) {
321
            $crop_w = $width;
322
            $crop_h = $width / $ratio;
323
            $crop_x = 0;
324
            $crop_y = ($height - $crop_h) / 2;
325
        } else {
326
            $crop_h = $height;
327
            $crop_w = $height * $ratio;
328
            $crop_y = 0;
329
            $crop_x = ($width - $crop_w) / 2;
330
        }
331
332
        return compact('crop_w', 'crop_h', 'crop_x', 'crop_y');
333
    }
334
}
335