Edge Case 1: Config Caching Hides Environment Changes
Symptom: Environment variable changes (like LOG_QUERY or REST_STRICT_COLUMNS) don’t take effect.Cause: Laravel caches configuration files for performance. With cached config, .env updates are ignored until the cache is rebuilt.
1
Reproduce the Issue
Set LOG_QUERY=true in .env
Run php artisan config:cache
Change LOG_QUERY=false in .env
Observe that queries are still being logged
2
Verify Current Config
// Check what the app actually seesdd(config('rest-generic-class.logging.query'));
// In a controller or tinkeruse Illuminate\Support\Facades\Config;// Before cache clearConfig::get('rest-generic-class.logging.query'); // true (old value)// After config:clearConfig::get('rest-generic-class.logging.query'); // false (new value)
Symptom: Long-running queue workers behave as if old configuration is still active, even after clearing cache.Cause: Queue workers boot the application once and keep config in memory until restarted.
# Graceful restart (waits for current jobs)php artisan queue:restart# Force stop and restart (systemd)sudo systemctl restart laravel-worker# Supervisorsudo supervisorctl restart laravel-worker:*
Symptom: Hierarchy listing requests timeout when working with large, deeply nested category trees.Cause: Without max_depth, the package recursively loads the entire tree, which can be thousands of records.
Edge Case 4: Excessive Filter Conditions Trigger Limits
Symptom: Requests fail with Maximum conditions (100) exceeded error.Cause: The filter engine enforces filtering.max_conditions to prevent database overload from complex filter trees.
Symptom: Two administrators update the same record simultaneously, and one set of changes is lost.Cause:update_multiple() applies updates row-by-row without record-level locking (last-write-wins).Reproduce the Issue:
POST /api/v1/products/update-multiple{ "product": [ {"id": 10, "stock": 50} ]}
3
Result
Final state depends on which request completes last. Admin 1’s price update might be lost.
Solution 1 - Optimistic Locking:
<?phpnamespace App\Services;use App\Models\Product;use Ronu\RestGenericClass\Core\Services\BaseService;class ProductService extends BaseService{ public function update($id, $payload) { $product = $this->modelClass->findOrFail($id); // Check updated_at to detect concurrent modifications if (isset($payload['updated_at'])) { $clientTimestamp = $payload['updated_at']; if ($product->updated_at->toIso8601String() !== $clientTimestamp) { throw new \Exception( 'Record was modified by another user. Please refresh and try again.' ); } unset($payload['updated_at']); } return parent::update($id, $payload); }}
Solution 2 - Database-Level Locking:
use Illuminate\Support\Facades\DB;public function update_multiple($items){ return DB::transaction(function () use ($items) { foreach ($items as $item) { // Lock the row for update $record = $this->modelClass ->where('id', $item['id']) ->lockForUpdate() ->first(); if ($record) { $record->update($item); } } });}
Symptom: Cached data briefly shows stale information after an update.Cause: Cache version bump happens after the database write, creating a small window where stale cache is valid.Mitigation:
// The package handles this internally, but you can add extra safety:public function update($id, $payload){ // Bump cache version BEFORE write $this->bumpCacheVersion(); try { $result = parent::update($id, $payload); } catch (\Exception $e) { // Rollback cache version if update fails $this->revertCacheVersion(); throw $e; } return $result;}
Symptom: Users from one tenant can access permissions from another tenant.Cause: Spatie’s PermissionRegistrar uses a team ID to scope permissions. If not set, it falls back to a global cache key.Test the Issue:
use Spatie\Permission\PermissionRegistrar;// Tenant A userauth()->setUser($tenantAUser);app(PermissionRegistrar::class)->setPermissionsTeamId(1);$tenantAUser->can('products.view'); // true// Tenant B user (forgot to set team)auth()->setUser($tenantBUser);// ⚠️ Team ID not set - uses global cache$tenantBUser->can('products.view'); // true (WRONG - should be false)
Solution - Set Team ID in Middleware:
<?phpnamespace App\Http\Middleware;use Closure;use Spatie\Permission\PermissionRegistrar;class SetPermissionTeam{ public function handle($request, Closure $next) { $user = $request->user(); if ($user && $user->tenant_id) { app(PermissionRegistrar::class) ->setPermissionsTeamId($user->tenant_id); } return $next($request); }}
Symptom: Requests return 400 error: Relation 'internalLogs' is not allowed.Cause: The relation exists on the model but isn’t added to the RELATIONS allowlist.
class Product extends BaseModel{ const RELATIONS = ['category', 'reviews']; // 'tags' relation exists but not allowlisted public function tags() { return $this->belongsToMany(Tag::class); }}
Test Validation:
use Tests\TestCase;class ProductTest extends TestCase{ public function test_rejects_invalid_relation() { $response = $this->postJson('/api/v1/products', [ 'relations' => ['internalLogs'], 'oper' => ['and' => ['status|=|active']] ]); $response->assertStatus(400) ->assertJson([ 'success' => false, 'message' => "Relation 'internalLogs' is not allowed" ]); }}
public function test_config_cache_affects_behavior(){ // Set config config(['rest-generic-class.filtering.strict_columns' => true]); // Cache config Artisan::call('config:cache'); // Change config (should be ignored) config(['rest-generic-class.filtering.strict_columns' => false]); // Assert cached value is used $this->assertTrue( config('rest-generic-class.filtering.strict_columns') );}