Skip to content

Commit

Permalink
added MetableAttributes trait
Browse files Browse the repository at this point in the history
  • Loading branch information
frasmage committed Apr 24, 2024
1 parent fab3910 commit 0c15bfb
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Version 6 contains a number of changes to improve the security and performance o
- A value of `null` is ignored by all cast types.
- Added `mergeMetaCasts()` method which can be used to override the defined cast on a meta key at runtime.

### Meta Attributes

- Added optional trait `MetableAttributes` which can further extend the `Metable` trait allowing access to meta values as model attributes using a `meta_` prefix. This can be useful for type hinting, IDE autocompletion, static analysis, and usage in Blade templates.

### Meta
- Added `$meta->string_value` and `$meta->numeric_value` attributes, which are used for optimizing queries filtering by meta value
- Added `$meta->hmac` attribute, which is used by some data type handlers to validate that the payload has not been tampered with.
Expand Down
4 changes: 4 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@

* Review the documentation about which data types can be queried with the various `whereMeta*` and `whereMeta*Numeric` query scopes. If you are querying the serialized `value` column directly, be aware that the formatting of array/object data types may have changed.

### Metable Attributes

* Optional: if you intend to access meta with property access, add the new `\Plank\Metable\MetableAttributes` traits to your `Metable`.

## 4.X -> 5.X
- New migration file added which adds a new composite unique index to the meta table on `metable_type`, `metable_id`, and `key`. Make sure that you have no duplicate keys for a given entity (previously possible as a race condition) before applying the new migration.

Expand Down
51 changes: 51 additions & 0 deletions docs/source/handling_meta.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,54 @@ You can also instruct your model class to `always` eager load the meta relations

protected $with = ['meta'];
}


Meta As Attributes
------------------

If you prefer to access meta as if they were attributes of the model, you can use the ``MetableAttributes`` trait insin addition to the ``Metable`` trait. This will allow you to access meta as if they were attributes of the model by prefixing them with ``meta_``. Meta attributes can be combined with type annotations, casts and/or default values to provide consistent typing. This can be useful for IDE completions and static analysis, as well as for use in Blade templates.

::

<?php

namespace App;

use Plank\Metable\Metable;
use Plank\Metable\MetableAttributes;
use Illuminate\Database\Eloquent\Model;

/**
* @property bool $meta_approved
* @property \Carbon\Carbon $meta_published_at
* @property int $meta_likes
*/
class Page extends Model
{
use Metable, MetableAttributes;

$metaCasts = [
'approved' => 'boolean',
'published_at' => 'datetime',
'likes' => 'integer',
];

$defaultMetaValues = [
'approved' => false,
'published_at' => null,
'likes' => 0,
];

// ...
}

$page = new Page();
$page->meta_likes = 5; // equivalent to $page->setMeta('likes', 5);
$page->fill(['meta_approved' => true, 'meta_published_at' => now()]); // equivalent to $page->setManyMeta([...]);
if ($page->meta_likes > 0) {} // equivalent $page->getMeta('likes');
unset($page->meta_likes); // equivalent to $page->removeMeta('likes');


Most attribute operations will translate meta attributes to their corresponding meta operations. However, the ``getAttributes()`` method will **not** include meta attributes. The ``getMetaAttributes()`` method can be used to retrieve all meta values keyed by their attribute name.

The ``toArray()`` method will include meta attributes by default. The ``$visible``/``$hidden`` properties of the model will be respected if any meta attributes are listed. The ``makeMetaHidden()`` method can be used to quickly hide all currently assigned meta attributes from the array representation of the model.
116 changes: 116 additions & 0 deletions src/MetableAttributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace Plank\Metable;

use Illuminate\Support\Collection;

trait MetableAttributes
{
public function getAttribute($key)
{
if ($this->isMetaAttribute($key)) {
return $this->getMeta($this->metaAttributeToKey($key));
}

return parent::getAttribute($key);
}

public function setAttribute($key, $value)
{
if ($this->isMetaAttribute($key)) {
$this->setMeta($this->metaAttributeToKey($key), $value);
return;
}

parent::setAttribute($key, $value);
}

public function fill(array $attributes)
{
foreach ($attributes as $key => $value) {
if ($this->isMetaAttribute($key)) {
$this->setMeta($this->metaAttributeToKey($key), $value);
unset($attributes[$key]);
}
}

parent::fill($attributes);
}

public function getMetaAttributes(): Collection
{
$attributes = [];
foreach ($this->getAllMeta() as $key => $value) {
$attributes[$this->metaKeyToAttribute($key)] = $value;
}
return collect($attributes);
}

public function offsetExists($key): bool
{
if ($this->isMetaAttribute($key)) {
return $this->hasMeta($this->metaAttributeToKey($key));
}

return parent::isset($key);
}

public function offsetUnset($key): void
{
if ($this->isMetaAttribute($key)) {
$this->removeMeta($this->metaAttributeToKey($key));
return;
}

parent::offsetUnset($key);
}

protected function isMetaAttribute($key): bool
{
return str_starts_with($key, 'meta_')
&& !array_key_exists($key, $this->attributes)
&& !array_key_exists($key, $this->casts);
}

public function toArray()
{
if (property_exists($this, 'includeMetaInArray')
&& !$this->includeMetaInArray
) {
return parent::toArray();
}

return array_merge(
parent::toArray(),
$this->getArrayableItems($this->getMetaAttributes()->toArray())
);
}

public function makeMetaHidden(): void
{
$this->hidden = array_merge(
$this->hidden,
array_keys($this->getMetaAttributes()->toArray())
);
}

protected function metaAttributeToKey(string $attribute): string
{
return substr($attribute, 5);
}

protected function metaKeyToAttribute(string $key): string
{
return 'meta_' . $key;
}

abstract public function getMeta(string $key, mixed $default = null): mixed;

abstract public function setMeta(string $key, mixed $value, bool $encrypt = false): void;

abstract public function removeMeta(string $key): void;

abstract public function getAllMeta(): \Illuminate\Support\Collection;

abstract protected function getArrayableItems(array $values);
}
96 changes: 96 additions & 0 deletions tests/Integration/MetableAttributesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Plank\Metable\Tests\Integration;

use Plank\Metable\Tests\Mocks\SampleMetable;
use Plank\Metable\Tests\TestCase;

class MetableAttributesTest extends TestCase
{
public function test_it_mutates_meta_attributes()
{
$this->useDatabase();

$model = $this->createMetable();
$this->assertFalse($model->hasMeta('var'));
$this->assertFalse(isset($model->meta_var));
$this->assertNull($model->getAttribute('meta_var'));

$model->setAttribute('meta_var', 'bar');
$this->assertEquals('bar', $model->getMeta('var'));
$this->assertTrue(isset($model->meta_var));
$this->assertEquals('bar', $model->getAttribute('meta_var'));

$model->meta_var = 'baz';
$this->assertEquals('baz', $model->getMeta('var'));
$this->assertTrue(isset($model->meta_var));
$this->assertEquals('baz', $model->getAttribute('meta_var'));

$model->offsetUnset('meta_var');
$this->assertFalse($model->hasMeta('var'));
$this->assertFalse(isset($model->meta_var));
$this->assertNull($model->getAttribute('meta_var'));

$model->fill(['meta_var' => 'qux']);
$this->assertEquals('qux', $model->getMeta('var'));
$this->assertTrue(isset($model->meta_var));
$this->assertEquals('qux', $model->getAttribute('meta_var'));

unset($model['meta_var']);
$this->assertFalse($model->hasMeta('var'));
$this->assertFalse(isset($model->meta_var));
$this->assertNull($model->getAttribute('meta_var'));
}

public function test_it_doesnt_overwrite_existing_attributes()
{
$this->useDatabase();

$model = $this->createMetable();
$model->meta_attribute = 'bar';
$this->assertFalse($model->hasMeta('attribute'));

$model->setAttribute('meta_attribute', 'baz');
$this->assertFalse($model->hasMeta('attribute'));
}

public function test_it_converts_to_array()
{
$this->useDatabase();
$model = $this->createMetable();
$model->meta_var = 'foo';
$model->meta_var2 = 'foo2';

$this->assertEquals(
collect([
'meta_var' => 'foo',
'meta_var2' => 'foo2',
'meta_foo' => 'bar' // default value
]),
$model->getMetaAttributes()
);

$model->makeHidden('meta_var2', 'created_at', 'updated_at', 'meta');

$array = $model->toArray();
$this->assertEquals([
'meta_attribute' => '',
'id' => $model->getKey(),
'meta_foo' => 'bar',
'meta_var' => 'foo'
], $array);

$model->makeMetaHidden();

$array = $model->toArray();
$this->assertEquals([
'meta_attribute' => '',
'id' => $model->getKey(),
], $array);
}

private function createMetable(array $attributes = []): SampleMetable
{
return factory(SampleMetable::class)->create($attributes);
}
}
2 changes: 1 addition & 1 deletion tests/Integration/MorphTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function test_it_can_join_correctly_from_morphed_class(): void
$results1 = SampleMorph::orderByMeta('foo', 'asc')->get();
$results2 = Meta::select('metable_type')->get();

$this->assertCount(3, $results1->pluck('id')->toArray());
$this->assertEquals([3, 1, 2], $results1->modelKeys());
$this->assertEquals([$class, $class, $class], $results2->pluck('metable_type')->toArray());
}

Expand Down
6 changes: 6 additions & 0 deletions tests/Mocks/SampleMetable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@

use Illuminate\Database\Eloquent\Model;
use Plank\Metable\Metable;
use Plank\Metable\MetableAttributes;
use Plank\Metable\MetableInterface;

class SampleMetable extends Model implements MetableInterface
{
use Metable;
use MetableAttributes;

protected $attributes = [
'meta_attribute' => '',
];

protected $defaultMetaValues = [
'foo' => 'bar'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function up()
{
Schema::create('sample_metables', function (Blueprint $table) {
$table->id();
$table->string('meta_attribute')->nullable();
$table->softDeletes();
$table->timestamps();
});
Expand Down

0 comments on commit 0c15bfb

Please sign in to comment.