Skip to content

Commit c7d824a

Browse files
timacdonaldnandi95taylorotwell
authored
Improve performance of setting multiple feature values at once (#157)
* feat!: add `setAll` method to driver * Support setAll in 1.x * Share same timestamp * Update Decorator.php --------- Co-authored-by: Nandor Kraszlan <nandor.kraszlan@bizclikmedia.com> Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent 3d360cd commit c7d824a

File tree

7 files changed

+292
-11
lines changed

7 files changed

+292
-11
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Laravel\Pennant\Contracts;
4+
5+
interface CanSetManyFeaturesForScopes
6+
{
7+
/**
8+
* Set multiple feature flag values.
9+
*
10+
* @param list<array{ feature: string, scope: mixed, value: mixed }> $features
11+
*/
12+
public function setAll(array $features): void;
13+
}

src/Drivers/ArrayDriver.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ public function set($feature, $scope, $value): void
158158
$this->resolvedFeatureStates[$feature][Feature::serializeScope($scope)] = $value;
159159
}
160160

161+
/**
162+
* Set multiple feature flag values.
163+
*
164+
* @param list<array{ feature: string, scope: mixed, value: mixed }> $features
165+
*/
166+
public function setAll(array $features): void
167+
{
168+
foreach ($features as $featureData) {
169+
$this->set($featureData['feature'], $featureData['scope'], $featureData['value']);
170+
}
171+
}
172+
161173
/**
162174
* Set a feature flag's value for all scopes.
163175
*

src/Drivers/DatabaseDriver.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
use Illuminate\Support\Carbon;
1111
use Illuminate\Support\Collection;
1212
use Laravel\Pennant\Contracts\CanListStoredFeatures;
13+
use Laravel\Pennant\Contracts\CanSetManyFeaturesForScopes;
1314
use Laravel\Pennant\Contracts\Driver;
1415
use Laravel\Pennant\Events\UnknownFeatureResolved;
1516
use Laravel\Pennant\Feature;
1617
use RuntimeException;
1718
use stdClass;
1819

19-
class DatabaseDriver implements CanListStoredFeatures, Driver
20+
class DatabaseDriver implements CanListStoredFeatures, CanSetManyFeaturesForScopes, Driver
2021
{
2122
/**
2223
* The database connection.
@@ -281,6 +282,24 @@ public function set($feature, $scope, $value): void
281282
], uniqueBy: ['name', 'scope'], update: ['value', static::UPDATED_AT]);
282283
}
283284

285+
/**
286+
* Set multiple feature flag values.
287+
*
288+
* @param list<array{ feature: string, scope: mixed, value: mixed }> $features
289+
*/
290+
public function setAll(array $features): void
291+
{
292+
$now = Carbon::now();
293+
294+
$this->newQuery()->upsert(array_map(fn (array $feature) => [
295+
'name' => $feature['feature'],
296+
'scope' => Feature::serializeScope($feature['scope']),
297+
'value' => json_encode($feature['value'], flags: JSON_THROW_ON_ERROR),
298+
static::CREATED_AT => $now,
299+
static::UPDATED_AT => $now,
300+
], $features), uniqueBy: ['name', 'scope'], update: ['value', static::UPDATED_AT]);
301+
}
302+
284303
/**
285304
* Set a feature flag's value for all scopes.
286305
*

src/Drivers/Decorator.php

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Support\Str;
1111
use Illuminate\Support\Traits\Macroable;
1212
use Laravel\Pennant\Contracts\CanListStoredFeatures;
13+
use Laravel\Pennant\Contracts\CanSetManyFeaturesForScopes;
1314
use Laravel\Pennant\Contracts\DefinesFeaturesExternally;
1415
use Laravel\Pennant\Contracts\Driver;
1516
use Laravel\Pennant\Contracts\FeatureScopeable;
@@ -37,7 +38,7 @@
3738
/**
3839
* @mixin \Laravel\Pennant\PendingScopedFeatureInteraction
3940
*/
40-
class Decorator implements CanListStoredFeatures, Driver, HasFlushableCache
41+
class Decorator implements CanListStoredFeatures, CanSetManyFeaturesForScopes, Driver, HasFlushableCache
4142
{
4243
use Macroable {
4344
__call as macroCall;
@@ -487,6 +488,44 @@ public function set($feature, $scope, $value): void
487488
Event::dispatch(new FeatureUpdated($feature, $scope, $value));
488489
}
489490

491+
/**
492+
* Set multiple feature flag values.
493+
*
494+
* @internal
495+
*
496+
* @param list<array{ feature: string, scope: mixed, value: mixed }> $features
497+
*/
498+
public function setAll(array $features): void
499+
{
500+
$features = array_map(fn ($feature) => [
501+
'feature' => $this->resolveFeature($feature['feature']),
502+
'scope' => $this->resolveScope($feature['scope']),
503+
'value' => $feature['value'],
504+
], $features);
505+
506+
$updated = false;
507+
508+
if ($this->driver instanceof CanSetManyFeaturesForScopes) {
509+
$this->driver->setAll($features);
510+
511+
$updated = true;
512+
}
513+
514+
foreach ($features as $feature) {
515+
if (! $updated) {
516+
$this->driver->set($feature['feature'], $feature['scope'], $feature['value']);
517+
}
518+
519+
$this->putInCache($feature['feature'], $feature['scope'], $feature['value']);
520+
521+
Event::dispatch(new FeatureUpdated(
522+
$feature['feature'],
523+
$feature['scope'],
524+
$feature['value'],
525+
));
526+
}
527+
}
528+
490529
/**
491530
* Activate the feature for everyone.
492531
*

src/PendingScopedFeatureInteraction.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,12 @@ public function unless($feature, $whenInactive, $whenActive = null)
249249
*/
250250
public function activate($feature, $value = true)
251251
{
252-
Collection::wrap($feature)
252+
$features = Collection::wrap($feature)
253253
->crossJoin($this->scope())
254-
->each(fn ($bits) => $this->driver->set($bits[0], $bits[1], $value));
254+
->map(fn ($bits) => ['feature' => $bits[0], 'scope' => $bits[1], 'value' => $value])
255+
->all();
256+
257+
$this->driver->setAll($features);
255258
}
256259

257260
/**
@@ -262,9 +265,12 @@ public function activate($feature, $value = true)
262265
*/
263266
public function deactivate($feature)
264267
{
265-
Collection::wrap($feature)
268+
$features = Collection::wrap($feature)
266269
->crossJoin($this->scope())
267-
->each(fn ($bits) => $this->driver->set($bits[0], $bits[1], false));
270+
->map(fn ($bits) => ['feature' => $bits[0], 'scope' => $bits[1], 'value' => false])
271+
->all();
272+
273+
$this->driver->setAll($features);
268274
}
269275

270276
/**

tests/Feature/ArrayDriverTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,96 @@ public function test_it_can_handles_float_scopes_correctly()
12681268
FloatScopeFeature::class => false,
12691269
], Feature::for(10.00)->all());
12701270
}
1271+
1272+
public function test_it_dispatches_events_when_activating_multiple_features_and_scopes()
1273+
{
1274+
Event::fake([FeatureUpdated::class]);
1275+
1276+
$first = new User(['id' => 1]);
1277+
$second = new User(['id' => 2]);
1278+
1279+
Feature::for([$first, $second])->activate(['foo', 'bar']);
1280+
1281+
Event::assertDispatchedTimes(FeatureUpdated::class, 4);
1282+
1283+
$events = [];
1284+
Event::assertDispatched(function (FeatureUpdated $event) use (&$events) {
1285+
$events[] = [
1286+
'feature' => $event->feature,
1287+
'scope' => $event->scope,
1288+
'value' => $event->value,
1289+
];
1290+
1291+
return true;
1292+
});
1293+
1294+
$this->assertCount(4, $events);
1295+
$this->assertContains([
1296+
'feature' => 'foo',
1297+
'scope' => $first,
1298+
'value' => true,
1299+
], $events);
1300+
$this->assertContains([
1301+
'feature' => 'foo',
1302+
'scope' => $second,
1303+
'value' => true,
1304+
], $events);
1305+
$this->assertContains([
1306+
'feature' => 'bar',
1307+
'scope' => $first,
1308+
'value' => true,
1309+
], $events);
1310+
$this->assertContains([
1311+
'feature' => 'bar',
1312+
'scope' => $second,
1313+
'value' => true,
1314+
], $events);
1315+
}
1316+
1317+
public function test_it_dispatches_events_when_deactivating_multiple_features_and_scopes()
1318+
{
1319+
Event::fake([FeatureUpdated::class]);
1320+
1321+
$first = new User(['id' => 1]);
1322+
$second = new User(['id' => 2]);
1323+
1324+
Feature::for([$first, $second])->deactivate(['foo', 'bar']);
1325+
1326+
Event::assertDispatchedTimes(FeatureUpdated::class, 4);
1327+
1328+
$events = [];
1329+
Event::assertDispatched(function (FeatureUpdated $event) use (&$events) {
1330+
$events[] = [
1331+
'feature' => $event->feature,
1332+
'scope' => $event->scope,
1333+
'value' => $event->value,
1334+
];
1335+
1336+
return true;
1337+
});
1338+
1339+
$this->assertCount(4, $events);
1340+
$this->assertContains([
1341+
'feature' => 'foo',
1342+
'scope' => $first,
1343+
'value' => false,
1344+
], $events);
1345+
$this->assertContains([
1346+
'feature' => 'foo',
1347+
'scope' => $second,
1348+
'value' => false,
1349+
], $events);
1350+
$this->assertContains([
1351+
'feature' => 'bar',
1352+
'scope' => $first,
1353+
'value' => false,
1354+
], $events);
1355+
$this->assertContains([
1356+
'feature' => 'bar',
1357+
'scope' => $second,
1358+
'value' => false,
1359+
], $events);
1360+
}
12711361
}
12721362

12731363
class MyFeature

0 commit comments

Comments
 (0)