Skip to content

Commit 95bcd3f

Browse files
committed
Merge remote-tracking branch 'origin/develop'
2 parents c2a1fd5 + c009bae commit 95bcd3f

162 files changed

Lines changed: 921 additions & 1255 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ai/rules/tests.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,19 @@ Use `$this->actingAsForApi($user)` in API tests and `$this->actingAs($user)` in
2424

2525
## Feature and unit test placement
2626
Feature tests live in `tests/Feature/`, grouped by the feature or resource under test. A resource with both API and web controllers splits its tests into `Api/` and `Ui/` subfolders matching the layer under test; cross-cutting feature-area suites stay flat in their folder. Unit tests live in `tests/Unit/`, grouped by the type under test (`Models/`, `Presenters/`, `Transformers/`...).
27+
28+
## Avoid calling truncate() in tests — use delete() or forceDelete()
29+
30+
Avoid using `Model::truncate()` or `DB::table(...)->truncate()` in a test. Use `Model::query()->delete()` (soft delete)
31+
or `Model::query()->forceDelete()` (hard delete) instead — both are DML and roll back normally.
32+
33+
Why: TRUNCATE is DDL, and MySQL implicitly commits on DDL. That destroys the transaction `LazilyRefreshDatabase` wraps
34+
each test in. Laravel spots the dead transaction at teardown, clears `RefreshDatabaseState::$migrated`, and re-runs
35+
all migrations before the next test — roughly 10s per occurrence on MySQL. SQLite hides the problem entirely
36+
because its DDL is transactional, so this only shows up on a MySQL run.
37+
38+
The same trap applies to any runtime DDL, notably `Schema::table()` — which is why creating a `CustomField` (its
39+
`created` hook does `ALTER TABLE assets`) is expensive on MySQL.
40+
41+
Tell: the test that runs the DDL looks fast; the *next* test pays. A test suddenly reporting ~1,690 queries is paying
42+
for a re-migration.

app/Console/Commands/LdapSync.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ public function handle()
5151
exit();
5252
}
5353

54-
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); // 600 seconds = 10 minutes
55-
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
54+
ini_set('max_execution_time', config('app.ldap_time_limit')); // 600 seconds = 10 minutes
55+
ini_set('memory_limit', config('app.ldap_memory_limit'));
5656

5757
// Single source of truth for internal-key => LDAP-attribute-name
5858
// lives on the Ldap model so parseAndMapLdapAttributes and this

app/Console/Commands/LdapTroubleshooter.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -627,13 +627,11 @@ private function timed_boolean_execute($function)
627627
posix_kill($parent_pid, SIGUSR2);
628628
}
629629
exit();
630-
break; // yes I know we don't need it.
631630
case -1:
632631
// couldn't fork
633632
$this->error('COULD NOT FORK - assuming failure');
634633

635634
return false;
636-
break; // I still know that we don't need it
637635
default:
638636
// we remain the 'parent', $pid is the PID of the forked process.
639637
$siginfo = [];
@@ -645,7 +643,6 @@ private function timed_boolean_execute($function)
645643

646644
return false;
647645
}
648-
break; // Yeah I get it already, shush.
649646
}
650647
}
651648

app/Console/Commands/MoveUploadsToNewDisk.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function handle()
8989
$type_count++;
9090
$filename = basename($logo);
9191
Storage::disk('public')->put('uploads/'.$filename, file_get_contents($logo));
92-
$this->info($type_count.'. LOGO: '.$filename.' was copied to '.env('PUBLIC_AWS_URL').'/uploads/'.$filename);
92+
$this->info($type_count.'. LOGO: '.$filename.' was copied to '.config('filesystems.disks.public_aws.url').'/uploads/'.$filename);
9393
}
9494

9595
$private_uploads['assets'] = glob('storage/private_uploads/assets'.'/*.*');

app/Console/Commands/ObjectImportCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public function __construct()
5454
*/
5555
public function handle()
5656
{
57-
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); // 600 seconds = 10 minutes
58-
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
57+
ini_set('max_execution_time', config('importer.time_limit')); // 600 seconds = 10 minutes
58+
ini_set('memory_limit', config('importer.memory_limit'));
5959

6060
$this->progressIndicator = new ProgressIndicator($this->output);
6161

app/Console/Commands/SystemBackup.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function __construct()
4141
*/
4242
public function handle()
4343
{
44-
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); // 600 seconds = 10 minutes
44+
ini_set('max_execution_time', config('backup.time_limit')); // 600 seconds = 10 minutes
4545

4646
if ($this->option('filename')) {
4747
$filename = $this->option('filename');

app/Console/Commands/ValidateAssets.php

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use App\Models\Asset;
66
use Illuminate\Console\Command;
7-
use Illuminate\Support\MessageBag;
87

98
class ValidateAssets extends Command
109
{
@@ -69,18 +68,7 @@ public function handle()
6968

7069
private function formatValidationErrors(Asset $asset): string
7170
{
72-
$errors = $asset->getErrors();
73-
$messages = [];
74-
75-
if ($errors instanceof MessageBag) {
76-
$messages = $errors->all();
77-
} elseif (is_array($errors)) {
78-
$messages = $errors;
79-
} else {
80-
$messages = [(string) $errors];
81-
}
82-
83-
$prefixedMessages = collect($messages)
71+
$prefixedMessages = collect($asset->getErrors()->all())
8472
->map(fn ($message) => trim((string) $message))
8573
->filter()
8674
->map(fn (string $message) => str_starts_with($message, '') ? $message : ''.$message)

app/Exceptions/Handler.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Illuminate\Http\Response;
1616
use Illuminate\Session\TokenMismatchException;
1717
use Illuminate\Support\Facades\Lang;
18-
use Illuminate\Support\Facades\Log;
1918
use Illuminate\Validation\ValidationException;
2019
use Intervention\Image\Exception\NotSupportedException;
2120
use JsonException;
@@ -60,14 +59,36 @@ class Handler extends ExceptionHandler
6059
public function report(Throwable $exception)
6160
{
6261
if ($this->shouldReport($exception)) {
63-
if (class_exists(Log::class)) {
64-
Log::error($exception);
65-
}
66-
6762
return parent::report($exception);
6863
}
6964
}
7065

66+
/**
67+
* Report a caught exception, and rethrow in dev when the underlying
68+
* cause is a programmer-error \Error (TypeError, ArgumentCountError,
69+
* etc.) so it fails loud with a stack trace in dev instead of hiding behind
70+
* a friendly "something went wrong" flash. Plain \Exception (sub)types
71+
* (QueryException, ItemStillHasAssets, etc.) *always* report + return so
72+
* bulk operations can keep swallowing runtime-data failures per row.
73+
*
74+
* We should use this as a drop-in for `report($e)` inside the wide-net
75+
* catch (\Throwable $e) blocks in the bulk destroy / import paths.
76+
*/
77+
public static function reportOrRethrow(Throwable $e): void
78+
{
79+
// Both APP_DEBUG must be on AND the environment must
80+
// not be production before the raw \Error is allowed to
81+
// escape past the friendly user-facing error.
82+
if (
83+
config('app.debug')
84+
&& ! app()->environment('production')
85+
&& $e instanceof \Error
86+
) {
87+
throw $e;
88+
}
89+
report($e);
90+
}
91+
7192
/**
7293
* Render an exception into an HTTP response.
7394
*
@@ -161,7 +182,7 @@ public function render($request, Throwable $e)
161182
// This is traaaaash but it handles models that are not found while using route model binding :(
162183
// The only alternative is to set that at *each* route, which is crazypants
163184
if ($e instanceof ModelNotFoundException) {
164-
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
185+
$ids = $e->getIds();
165186

166187
if (in_array('bulkedit', $ids, true)) {
167188
$error_array = session()->get('bulk_asset_errors');
@@ -198,7 +219,7 @@ public function render($request, Throwable $e)
198219
// Normalize the space-separated derived name to underscore
199220
// so compound class names (AssetModel -> "asset model" -> "asset_model")
200221
// resolve to keys that actually exist in general.php.
201-
$translationKey = 'general.' . str_replace(' ', '_', $model_name);
222+
$translationKey = 'general.'.str_replace(' ', '_', $model_name);
202223
$translatedName = Lang::has($translationKey) ? trans($translationKey) : $model_name;
203224

204225
return redirect()

app/Helpers/Helper.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,7 +1642,6 @@ public static function isDemoMode()
16421642
{
16431643
if (config('app.lock_passwords') === true) {
16441644
return true;
1645-
Log::debug('app locked!');
16461645
}
16471646

16481647
return false;
@@ -1699,8 +1698,6 @@ public static function getUnitConversionFactor($unit)
16991698
return (1 / 72) * static::getUnitConversionFactor('in');
17001699
default:
17011700
throw new \InvalidArgumentException('Unit: '.e($unit).' is not supported');
1702-
1703-
return false;
17041701
}
17051702
}
17061703

@@ -1846,6 +1843,7 @@ public static function getRedirectOption($request, $id, $table, $item_id = null)
18461843
'Components' => route('components.index'),
18471844
'Consumables' => route('consumables.index'),
18481845
'Maintenances' => route('maintenances.index'),
1846+
default => route('home'),
18491847
};
18501848

18511849
// #15214: preserve query-string filters when the user came
@@ -1871,6 +1869,7 @@ public static function getRedirectOption($request, $id, $table, $item_id = null)
18711869
'Accessories' => redirect()->route('accessories.show', $id ?? $item_id),
18721870
'Components' => redirect()->route('components.show', $id ?? $item_id),
18731871
'Consumables' => redirect()->route('consumables.show', $id ?? $item_id),
1872+
default => redirect()->route('home'),
18741873
};
18751874
}
18761875

@@ -1890,6 +1889,7 @@ public static function getRedirectOption($request, $id, $table, $item_id = null)
18901889
'asset' => $assetId
18911890
? redirect()->route('hardware.show', $assetId)
18921891
: redirect()->route('hardware.index'),
1892+
default => redirect()->route('home'),
18931893
};
18941894
}
18951895

@@ -1898,6 +1898,7 @@ public static function getRedirectOption($request, $id, $table, $item_id = null)
18981898
return match ($other_redirect) {
18991899
'audit' => redirect()->route('assets.audit.due'),
19001900
'model' => redirect()->route('models.show', $request->model_id),
1901+
default => redirect()->route('home'),
19011902
};
19021903

19031904
}

app/Http/Controllers/Accessories/AccessoryCheckinController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public function store(Request $request, $accessoryCheckoutId = null, $backto = n
6767
'App\Models\User' => 'user',
6868
'App\Models\Location' => 'location',
6969
'App\Models\Asset' => 'asset',
70+
default => null,
7071
});
7172

7273
$checkin_hours = date('H:i:s');

0 commit comments

Comments
 (0)