Skip to content

Commit

Permalink
Merge pull request #104 from bfiessinger/fix/getMeta-empty-if-propert…
Browse files Browse the repository at this point in the history
…y-is-casted

[2.2.1] Fix getMeta returns empty Collection if attribute is casted in some cases
  • Loading branch information
kodeine authored Jul 5, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents a07765c + 4b47533 commit fcdfb7b
Showing 9 changed files with 237 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Release Notes

## v2.2.1

### Fixed

* Fixed a bug that was caused by a null value or a null cast. [PR #104](https://github.com/kodeine/laravel-meta/pull/104)

## v2.2.0

### Added
9 changes: 9 additions & 0 deletions src/Kodeine/Metable/Metable.php
Original file line number Diff line number Diff line change
@@ -442,6 +442,15 @@ public function getAttribute($key) {
return $attr;
}

// It is possible that attribute exists, or it has a cast, but it's null, so we check for that
if ( array_key_exists( $key, $this->attributes ) ||
array_key_exists( $key, $this->casts ) ||
$this->hasGetMutator( $key ) ||
$this->hasAttributeMutator( $key ) ||
$this->isClassCastable( $key ) ) {
return $attr;
}

// If key is a relation name, then return parent value.
// The reason for this is that it's possible that the relation does not exist and parent call returns null for that.
if ( $this->isRelation( $key ) && $this->relationLoaded( $key ) ) {
46 changes: 46 additions & 0 deletions tests/Casts/StateCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Kodeine\Metable\Tests\Casts;

use Exception;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class StateCaster implements CastsAttributes
{
private $baseStateClass;

public function __construct(string $baseStateClass) {
$this->baseStateClass = $baseStateClass;
}

public function get($model, $key, $value, $attributes) {
if ( is_null( $value ) ) return null;

if ( ! is_subclass_of( $value, $this->baseStateClass ) ) {
return null;
}

$stateClassName = $value::config()['default'];

return new $stateClassName( $model );
}

/**
* @throws Exception
*/
public function set($model, $key, $value, $attributes) {
if ( is_null( $value ) ) return null;

if ( ! is_subclass_of( $value, $this->baseStateClass ) ) {
throw new Exception( 'Invalid state class.' );
}

$value = new $value( $model );

if ( $value instanceof $this->baseStateClass ) {
$value->setField( $key );
}

return $value->getMorphClass();
}
}
46 changes: 46 additions & 0 deletions tests/Casts/UserCastedObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Kodeine\Metable\Tests\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;

abstract class UserCastedObject implements Castable
{
public static $name;
public $model;
public $stateConfig;
public $field;

public function __construct($model) {
$this->model = $model;
$this->stateConfig = static::config();
}

public static function config() {
return [
'default' => null,
];
}

public static function castUsing(array $arguments) {
return new StateCaster( static::class );
}

public function setField(string $field): self {
$this->field = $field;

return $this;
}

public static function getMorphClass(): string {
return static::$name ?? static::class;
}

public function make(?string $name, $model) {
if ( is_null( $name ) ) {
return null;
}

return new $name( $model );
}
}
17 changes: 17 additions & 0 deletions tests/Casts/UserState/DefaultState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Kodeine\Metable\Tests\Casts\UserState;

class DefaultState extends State
{
public $description;

/** @noinspection PhpMissingParentConstructorInspection */
public function __construct() {
$this->description = $this->description();
}

public function description(): string {
return 'This is a default description.';
}
}
17 changes: 17 additions & 0 deletions tests/Casts/UserState/State.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Kodeine\Metable\Tests\Casts\UserState;

use Kodeine\Metable\Tests\Casts\UserCastedObject;

abstract class State extends UserCastedObject
{
abstract public function description(): string;

public static function config()
{
return [
'default' => DefaultState::class,
];
}
}
32 changes: 32 additions & 0 deletions tests/MetableTest.php
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@
use Kodeine\Metable\Tests\Models\UserTest;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Kodeine\Metable\Tests\Casts\UserState\DefaultState;

class MetableTest extends TestCase
{
@@ -31,6 +32,8 @@ public static function setUpBeforeClass(): void {
$table->string( 'name' )->default( 'john' );
$table->string( 'email' )->default( '[email protected]' );
$table->string( 'password' )->nullable();
$table->string( 'state' )->nullable();
$table->string( 'null_value' )->nullable();
$table->integer( 'user_test_id' )->unsigned()->nullable();
$table->foreign( 'user_test_id' )->references( 'id' )->on( 'user_tests' );
$table->timestamps();
@@ -47,6 +50,16 @@ public static function setUpBeforeClass(): void {
} );
}

public function testCast() {
$user = new UserTest;

$this->assertNull( $user->state, 'Casted object should be null by default' );

$user->state = DefaultState::class;

$this->assertInstanceOf( DefaultState::class, $user->state, 'Casted object should be instanceof DefaultState' );
}

public function testFluentMeta() {
$user = new UserTest;

@@ -112,6 +125,21 @@ public function testFluentMeta() {
$this->assertTrue( $user->isMetaDirty( 'foo', 'bar' ), 'isMetaDirty should return true even if one of metas has changed' );
$this->assertTrue( $user->isMetaDirty( 'foo,bar' ), 'isMetaDirty should return true even if one of metas has changed' );

//re retrieve user from database
/** @var UserTest $user */
$user = UserTest::find( $user->id );

$this->assertNull( $user->null_value, 'null_value property should be null' );
$this->assertNull( $user->null_cast, 'null_cast property should be null' );

$user->setMeta( 'null_value', true );
$user->setMeta( 'null_cast', true );

$this->assertTrue( $user->getMeta( 'null_value' ), 'Meta should be set' );
$this->assertTrue( $user->getMeta( 'null_cast' ), 'Meta should be set' );
$this->assertNull( $user->null_value, 'null_value property should be null' );
$this->assertNull( $user->null_cast, 'null_cast property should be null' );

$user->delete();

$this->assertEquals( 0, $metaData->count(), 'Meta should be deleted from database after deleting user.' );
@@ -227,6 +255,10 @@ public function testMetaMethods() {

$user->save();

$meta = $user->getMeta();
$this->assertInstanceOf( 'Illuminate\Support\Collection', $meta, 'Meta method getMeta is not typeof Collection' );
$this->assertTrue( $meta->isNotEmpty(), 'Meta method getMeta did return empty collection' );

// re retrieve user to make sure meta is saved
$user = UserTest::with( ['metas'] )->find( $user->getKey() );

11 changes: 11 additions & 0 deletions tests/Models/UserTest.php
Original file line number Diff line number Diff line change
@@ -6,10 +6,13 @@
use Illuminate\Events\Dispatcher;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Kodeine\Metable\Tests\Casts\UserState\State;
use Kodeine\Metable\Tests\Traits\HasUserStates;

class UserTest extends Model
{
use Metable;
use HasUserStates;

public $defaultMetaValues = [
'default_meta_key' => 'default_meta_value',
@@ -19,6 +22,14 @@ class UserTest extends Model

public $disableFluentMeta = false;

protected $casts = [
'state' => State::class,
];

public function getNullCastAttribute() {
return null;
}

/**
* This is dummy relation to itself.
*
53 changes: 53 additions & 0 deletions tests/Traits/HasUserStates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Kodeine\Metable\Tests\Traits;

use Kodeine\Metable\Tests\Casts\UserState\State;
use Kodeine\Metable\Tests\Casts\UserCastedObject;

trait HasUserStates
{
public $casted = [
'state' => State::class,
];

public static function bootHasUserCasts() {
self::creating( function ($model) {
$model->setDefaultCastedProperties();
} );
}

public function initializeHasUserCasts() {
$this->setDefaultCastedProperties();
}

private function getStateConfigs() {
$casts = $this->getCasts();

$states = [];

foreach ($casts as $prop => $state) {
if ( ! is_subclass_of( $state, UserCastedObject::class ) ) {
continue;
}

$states[$prop] = $state::config();
}

return $states;
}

private function setDefaultCastedProperties() {
foreach ($this->getStateConfigs() as $prop => $config) {
if ( $this->{$prop} === null ) {
continue;
}

if ( ! isset( $config['default'] ) ) {
continue;
}

$this->{$prop} = $config['default'];
}
}
}

0 comments on commit fcdfb7b

Please sign in to comment.