Skip to content

Commit fcdfb7b

Browse files
authored
Merge pull request #104 from bfiessinger/fix/getMeta-empty-if-property-is-casted
[2.2.1] Fix getMeta returns empty Collection if attribute is casted in some cases
2 parents a07765c + 4b47533 commit fcdfb7b

File tree

9 files changed

+237
-0
lines changed

9 files changed

+237
-0
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release Notes
22

3+
## v2.2.1
4+
5+
### Fixed
6+
7+
* Fixed a bug that was caused by a null value or a null cast. [PR #104](https://github.com/kodeine/laravel-meta/pull/104)
8+
39
## v2.2.0
410

511
### Added

src/Kodeine/Metable/Metable.php

+9
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,15 @@ public function getAttribute($key) {
442442
return $attr;
443443
}
444444

445+
// It is possible that attribute exists, or it has a cast, but it's null, so we check for that
446+
if ( array_key_exists( $key, $this->attributes ) ||
447+
array_key_exists( $key, $this->casts ) ||
448+
$this->hasGetMutator( $key ) ||
449+
$this->hasAttributeMutator( $key ) ||
450+
$this->isClassCastable( $key ) ) {
451+
return $attr;
452+
}
453+
445454
// If key is a relation name, then return parent value.
446455
// The reason for this is that it's possible that the relation does not exist and parent call returns null for that.
447456
if ( $this->isRelation( $key ) && $this->relationLoaded( $key ) ) {

tests/Casts/StateCaster.php

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Kodeine\Metable\Tests\Casts;
4+
5+
use Exception;
6+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
7+
8+
class StateCaster implements CastsAttributes
9+
{
10+
private $baseStateClass;
11+
12+
public function __construct(string $baseStateClass) {
13+
$this->baseStateClass = $baseStateClass;
14+
}
15+
16+
public function get($model, $key, $value, $attributes) {
17+
if ( is_null( $value ) ) return null;
18+
19+
if ( ! is_subclass_of( $value, $this->baseStateClass ) ) {
20+
return null;
21+
}
22+
23+
$stateClassName = $value::config()['default'];
24+
25+
return new $stateClassName( $model );
26+
}
27+
28+
/**
29+
* @throws Exception
30+
*/
31+
public function set($model, $key, $value, $attributes) {
32+
if ( is_null( $value ) ) return null;
33+
34+
if ( ! is_subclass_of( $value, $this->baseStateClass ) ) {
35+
throw new Exception( 'Invalid state class.' );
36+
}
37+
38+
$value = new $value( $model );
39+
40+
if ( $value instanceof $this->baseStateClass ) {
41+
$value->setField( $key );
42+
}
43+
44+
return $value->getMorphClass();
45+
}
46+
}

tests/Casts/UserCastedObject.php

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Kodeine\Metable\Tests\Casts;
4+
5+
use Illuminate\Contracts\Database\Eloquent\Castable;
6+
7+
abstract class UserCastedObject implements Castable
8+
{
9+
public static $name;
10+
public $model;
11+
public $stateConfig;
12+
public $field;
13+
14+
public function __construct($model) {
15+
$this->model = $model;
16+
$this->stateConfig = static::config();
17+
}
18+
19+
public static function config() {
20+
return [
21+
'default' => null,
22+
];
23+
}
24+
25+
public static function castUsing(array $arguments) {
26+
return new StateCaster( static::class );
27+
}
28+
29+
public function setField(string $field): self {
30+
$this->field = $field;
31+
32+
return $this;
33+
}
34+
35+
public static function getMorphClass(): string {
36+
return static::$name ?? static::class;
37+
}
38+
39+
public function make(?string $name, $model) {
40+
if ( is_null( $name ) ) {
41+
return null;
42+
}
43+
44+
return new $name( $model );
45+
}
46+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Kodeine\Metable\Tests\Casts\UserState;
4+
5+
class DefaultState extends State
6+
{
7+
public $description;
8+
9+
/** @noinspection PhpMissingParentConstructorInspection */
10+
public function __construct() {
11+
$this->description = $this->description();
12+
}
13+
14+
public function description(): string {
15+
return 'This is a default description.';
16+
}
17+
}

tests/Casts/UserState/State.php

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Kodeine\Metable\Tests\Casts\UserState;
4+
5+
use Kodeine\Metable\Tests\Casts\UserCastedObject;
6+
7+
abstract class State extends UserCastedObject
8+
{
9+
abstract public function description(): string;
10+
11+
public static function config()
12+
{
13+
return [
14+
'default' => DefaultState::class,
15+
];
16+
}
17+
}

tests/MetableTest.php

+32
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Kodeine\Metable\Tests\Models\UserTest;
1111
use Illuminate\Database\Capsule\Manager as Capsule;
1212
use Illuminate\Database\Eloquent\ModelNotFoundException;
13+
use Kodeine\Metable\Tests\Casts\UserState\DefaultState;
1314

1415
class MetableTest extends TestCase
1516
{
@@ -31,6 +32,8 @@ public static function setUpBeforeClass(): void {
3132
$table->string( 'name' )->default( 'john' );
3233
$table->string( 'email' )->default( '[email protected]' );
3334
$table->string( 'password' )->nullable();
35+
$table->string( 'state' )->nullable();
36+
$table->string( 'null_value' )->nullable();
3437
$table->integer( 'user_test_id' )->unsigned()->nullable();
3538
$table->foreign( 'user_test_id' )->references( 'id' )->on( 'user_tests' );
3639
$table->timestamps();
@@ -47,6 +50,16 @@ public static function setUpBeforeClass(): void {
4750
} );
4851
}
4952

53+
public function testCast() {
54+
$user = new UserTest;
55+
56+
$this->assertNull( $user->state, 'Casted object should be null by default' );
57+
58+
$user->state = DefaultState::class;
59+
60+
$this->assertInstanceOf( DefaultState::class, $user->state, 'Casted object should be instanceof DefaultState' );
61+
}
62+
5063
public function testFluentMeta() {
5164
$user = new UserTest;
5265

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

128+
//re retrieve user from database
129+
/** @var UserTest $user */
130+
$user = UserTest::find( $user->id );
131+
132+
$this->assertNull( $user->null_value, 'null_value property should be null' );
133+
$this->assertNull( $user->null_cast, 'null_cast property should be null' );
134+
135+
$user->setMeta( 'null_value', true );
136+
$user->setMeta( 'null_cast', true );
137+
138+
$this->assertTrue( $user->getMeta( 'null_value' ), 'Meta should be set' );
139+
$this->assertTrue( $user->getMeta( 'null_cast' ), 'Meta should be set' );
140+
$this->assertNull( $user->null_value, 'null_value property should be null' );
141+
$this->assertNull( $user->null_cast, 'null_cast property should be null' );
142+
115143
$user->delete();
116144

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

228256
$user->save();
229257

258+
$meta = $user->getMeta();
259+
$this->assertInstanceOf( 'Illuminate\Support\Collection', $meta, 'Meta method getMeta is not typeof Collection' );
260+
$this->assertTrue( $meta->isNotEmpty(), 'Meta method getMeta did return empty collection' );
261+
230262
// re retrieve user to make sure meta is saved
231263
$user = UserTest::with( ['metas'] )->find( $user->getKey() );
232264

tests/Models/UserTest.php

+11
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
use Illuminate\Events\Dispatcher;
77
use Illuminate\Database\Eloquent\Model;
88
use Illuminate\Database\Eloquent\Relations\HasOne;
9+
use Kodeine\Metable\Tests\Casts\UserState\State;
10+
use Kodeine\Metable\Tests\Traits\HasUserStates;
911

1012
class UserTest extends Model
1113
{
1214
use Metable;
15+
use HasUserStates;
1316

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

2023
public $disableFluentMeta = false;
2124

25+
protected $casts = [
26+
'state' => State::class,
27+
];
28+
29+
public function getNullCastAttribute() {
30+
return null;
31+
}
32+
2233
/**
2334
* This is dummy relation to itself.
2435
*

tests/Traits/HasUserStates.php

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Kodeine\Metable\Tests\Traits;
4+
5+
use Kodeine\Metable\Tests\Casts\UserState\State;
6+
use Kodeine\Metable\Tests\Casts\UserCastedObject;
7+
8+
trait HasUserStates
9+
{
10+
public $casted = [
11+
'state' => State::class,
12+
];
13+
14+
public static function bootHasUserCasts() {
15+
self::creating( function ($model) {
16+
$model->setDefaultCastedProperties();
17+
} );
18+
}
19+
20+
public function initializeHasUserCasts() {
21+
$this->setDefaultCastedProperties();
22+
}
23+
24+
private function getStateConfigs() {
25+
$casts = $this->getCasts();
26+
27+
$states = [];
28+
29+
foreach ($casts as $prop => $state) {
30+
if ( ! is_subclass_of( $state, UserCastedObject::class ) ) {
31+
continue;
32+
}
33+
34+
$states[$prop] = $state::config();
35+
}
36+
37+
return $states;
38+
}
39+
40+
private function setDefaultCastedProperties() {
41+
foreach ($this->getStateConfigs() as $prop => $config) {
42+
if ( $this->{$prop} === null ) {
43+
continue;
44+
}
45+
46+
if ( ! isset( $config['default'] ) ) {
47+
continue;
48+
}
49+
50+
$this->{$prop} = $config['default'];
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)