Skip to content
This repository was archived by the owner on Feb 16, 2022. It is now read-only.

Clearing the Attributable static "cache" when creating attributes #132

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
composer.lock
composer.phar
phpunit.xml
.phpunit.result.cache
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"require-dev": {
"codedungeon/phpunit-result-printer": "^0.30.0",
"illuminate/container": "^8.0.0 || ^9.0.0",
"laravel/legacy-factories": "^1.1",
"orchestra/testbench": "^6.7.0",
"phpunit/phpunit": "^9.5.0"
},
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<testsuite name="Rinvex Attributes Feature Test Suite">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Rinvex Attributes Stress Test Suite">
<directory suffix="Test.php">./tests/Stress</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
Expand Down
58 changes: 51 additions & 7 deletions src/Models/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ class Attribute extends Model implements Sortable
'order_column_name' => 'sort_order',
];

/**
* The entities that need to be attached to this attribute.
*
* @var mixed
*/
protected $entitiesToSave = false;

/**
* The default rules that the model will validate against.
*
Expand All @@ -128,6 +135,31 @@ class Attribute extends Model implements Sortable
*/
protected static $typeMap = [];

/**
* {@inheritdoc}
*/
public static function boot()
{
parent::boot();
static::saved(function ($attribute) {
if ($attribute->entitiesToSave !== false) {
// Wrap this in a transaction so that we don't lose attached entities if `createMany` fails.
\DB::transaction(function () use ($attribute) {
$entities = $attribute->entitiesToSave ?: [];
$attribute->entities()->delete();
! $entities || $attribute->entities()->createMany(array_map(function ($entity) {
return ['entity_type' => $entity];
}, $entities));
$attribute->entitiesToSave = false;
});
}
$attribute->clearAttributableCache();
});
static::deleted(function ($attribute) {
$attribute->clearAttributableCache();
});
}

/**
* Create a new Eloquent model instance.
*
Expand Down Expand Up @@ -201,7 +233,7 @@ public static function getTypeModel($alias)
*/
public function getEntitiesAttribute(): array
{
return $this->entities()->pluck('entity_type')->toArray();
return $this->entities()->pluck('entity_type')->toArray() ?: $this->entitiesToSave ?: [];
}

/**
Expand All @@ -214,12 +246,7 @@ public function getEntitiesAttribute(): array
*/
public function setEntitiesAttribute($entities): void
{
static::saved(function ($model) use ($entities) {
$this->entities()->delete();
! $entities || $this->entities()->createMany(array_map(function ($entity) {
return ['entity_type' => $entity];
}, $entities));
});
$this->entitiesToSave = (array) $entities;
}

/**
Expand Down Expand Up @@ -257,4 +284,21 @@ public function values(string $value): HasMany
{
return $this->hasMany($value, 'attribute_id', 'id');
}

/**
* Clears the attributable cache for all entities that are attached to this attribute.
*
* @return void
*/
public function clearAttributableCache()
{
foreach ($this->entities as $entity) {
// Ensure that the class exists before creating an instance as the database
// could contain a model that no longer exists.
if (class_exists($entity)) {
$entityInstance = app()->make($entity);
$entityInstance->clearAttributableCache();
}
}
}
}
13 changes: 13 additions & 0 deletions src/Traits/Attributable.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,19 @@ public function getEntityAttributes(): Collection
return static::$entityAttributes->get($morphClass) ?? new Collection();
}

/**
* Clear the static attributes cache for this model.
*
* @return void
*/
public function clearAttributableCache()
{
$morphClass = $this->getMorphClass();
if (static::$entityAttributes && static::$entityAttributes->has($morphClass)) {
static::$entityAttributes->forget($morphClass);
}
}

/**
* Get the fillable attributes of a given array.
*
Expand Down
25 changes: 24 additions & 1 deletion tests/Feature/AttributeCreationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

namespace Rinvex\Attributes\Tests\Feature;

use Rinvex\Attributes\Tests\Stubs\User;
use Rinvex\Attributes\Tests\TestCase;
use Rinvex\Attributes\Tests\Models\User;

class AttributeCreationTest extends TestCase
{
Expand Down Expand Up @@ -52,6 +53,28 @@ public function it_ensures_unique_slugs_even_if_slugs_explicitly_provided()
$this->assertDatabaseHas('attributes', ['slug' => 'foo_1']);
}

/** @test */
public function it_ensures_attributable_cache_will_clear()
{
// Create an attribute.
$this->createAttribute(['slug' => 'foo']);
$this->assertDatabaseHas('attributes', ['slug' => 'foo']);

$user = app()->make(User::class);
$this->assertEquals(1, $user->getEntityAttributes()->count());

// Create another attribute.
$this->createAttribute(['slug' => 'bar']);
$this->assertDatabaseHas('attributes', ['slug' => 'bar']);
$this->assertEquals(2, $user->getEntityAttributes()->count());

// Create three more.
$this->createAttribute(['slug' => 'baz']);
$this->createAttribute(['slug' => 'beans']);
$this->createAttribute(['slug' => 'blorg']);
$this->assertEquals(5, $user->getEntityAttributes()->count());
}

protected function createAttribute($attributes = [])
{
return app('rinvex.attributes.attribute')->create(array_merge([
Expand Down
75 changes: 75 additions & 0 deletions tests/Feature/AttributeValuesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Rinvex\Attributes\Tests\Feature;

use Rinvex\Attributes\Tests\TestCase;
use Rinvex\Attributes\Tests\Models\Thing;

class AttributeValuesTest extends TestCase
{
/**
* Test basic EAV functionality.
*
* @return void
*/
public function test_basic_eav_functionality()
{
$this->createAttributes();
$size = 'small';
$colour = 'red';
$featured = true;
$thing = factory(Thing::class, 1)->create(['code' => 'EAVTEST'])->first();
$thing->size = $size;
$thing->colour = $colour;
$thing->featured = $featured;
$thing->save();
// Fetch the thing again and check that the size, colour and featured have been saved.
$thing = Thing::where('code', 'EAVTEST')->first();
// Check that the thing exists.
$this->assertDatabaseHas('things', [
'code' => 'EAVTEST',
]);
// Check that the size, colour and featured are as expected.
$this->assertEquals($size, $thing->size);
$this->assertEquals($colour, $thing->colour);
$this->assertEquals($featured, $thing->featured);
}

/**
* Create EAV attributes to use in tests.
*
* @return void
*/
protected function createAttributes()
{
app('rinvex.attributes.attribute')->create([
'slug' => 'size',
'type' => 'varchar',
'name' => 'Thing Size',
'entities' => [Thing::class],
]);
app('rinvex.attributes.attribute')->create([
'slug' => 'colour',
'type' => 'varchar',
'name' => 'Thing Colour',
'entities' => [Thing::class],
]);
app('rinvex.attributes.attribute')->create([
'slug' => 'featured',
'type' => 'bool',
'name' => 'Is Thing Featured',
'entities' => [Thing::class],
]);
$this->assertDatabaseHas('attributes', [
'slug' => 'size',
]);
$this->assertDatabaseHas('attributes', [
'slug' => 'colour',
]);
$this->assertDatabaseHas('attributes', [
'slug' => 'featured',
]);
}
}
13 changes: 13 additions & 0 deletions tests/Models/Thing.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Rinvex\Attributes\Tests\Models;

use Illuminate\Database\Eloquent\Model;
use Rinvex\Attributes\Traits\Attributable;

class Thing extends Model
{
use Attributable;
}
2 changes: 1 addition & 1 deletion tests/Stubs/User.php → tests/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Rinvex\Attributes\Tests\Stubs;
namespace Rinvex\Attributes\Tests\Models;

use Illuminate\Database\Eloquent\Model;
use Rinvex\Attributes\Traits\Attributable;
Expand Down
64 changes: 64 additions & 0 deletions tests/Stress/AttributeStressTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Rinvex\Attributes\Tests\Stress;

use Faker\Generator as Faker;
use Rinvex\Attributes\Tests\TestCase;
use Rinvex\Attributes\Models\Attribute;
use Rinvex\Attributes\Tests\Models\User;
use Rinvex\Attributes\Tests\Models\Thing;

class AttributeStressTest extends TestCase
{
/**
* Create ten attributes.
*
* @return void
*/
public function test_ten_attributes()
{
// TODO: Fix the issue with attaching entities to attributes so that we can use the factory instead.
// factory(Attribute::class, 10)->create(['entities' => [Thing::class, User::class]])->each(function ($attribute) {
// // ...
// });
$faker = app()->make(Faker::class);
for ($i = 0; $i < 10; $i++) {
$attributes[] = $faker->unique()->slug(2);
}
foreach ($attributes as $attribute) {
app('rinvex.attributes.attribute')->create([
'slug' => $attribute,
'type' => $faker->randomElement(['boolean', 'datetime', 'integer', 'text', 'varchar']),
'name' => 'Thing '.ucfirst($attribute),
'entities' => [Thing::class, User::class],
]);
}

$this->assertDatabaseCount('attributes', 10);
}

/**
* Create one hundred attributes.
*
* @return void
*/
public function test_one_hundred_attributes()
{
$faker = app()->make(Faker::class);
for ($i = 0; $i < 100; $i++) {
$attributes[] = $faker->unique()->slug(2);
}
foreach ($attributes as $attribute) {
app('rinvex.attributes.attribute')->create([
'slug' => $attribute,
'type' => $faker->randomElement(['boolean', 'datetime', 'integer', 'text', 'varchar']),
'name' => 'Thing '.ucfirst($attribute),
'entities' => [Thing::class, User::class],
]);
}

$this->assertDatabaseCount('attributes', 100);
}
}
13 changes: 10 additions & 3 deletions tests/Feature/TestCase.php → tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@

declare(strict_types=1);

namespace Rinvex\Attributes\Tests\Feature;
namespace Rinvex\Attributes\Tests;

use Rinvex\Attributes\Models\Attribute;
use Rinvex\Attributes\Tests\Stubs\User;
use Rinvex\Attributes\Tests\Models\User;
use Rinvex\Attributes\Tests\Models\Thing;
use Rinvex\Support\Providers\SupportServiceProvider;
use Rinvex\Attributes\Providers\AttributesServiceProvider;
use Illuminate\Database\Eloquent\Factory as EloquentFactory;

class TestCase extends \Orchestra\Testbench\TestCase
{
protected function setUp(): void
{
parent::setUp();

$this->loadMigrationsFrom(__DIR__.'/database/migrations');
$this->artisan('migrate', ['--database' => 'testing']);
$this->loadLaravelMigrations('testing');
$this->withFactories(__DIR__.'/Factories');
$this->app->make(EloquentFactory::class)->load(__DIR__.'/database/factories');
$this->app->make(EloquentFactory::class)->load(dirname(__DIR__).'/database/factories');

// Registering the core type map
Attribute::typeMap([
Expand All @@ -29,6 +34,7 @@ protected function setUp(): void

// Push your entity fully qualified namespace
app('rinvex.attributes.entities')->push(User::class);
app('rinvex.attributes.entities')->push(Thing::class);
}

protected function getEnvironmentSetUp($app)
Expand All @@ -45,6 +51,7 @@ protected function getPackageProviders($app)
{
return [
AttributesServiceProvider::class,
SupportServiceProvider::class,
];
}
}
Loading