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
|
|||||
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
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
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 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
|
|||||
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 |
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.