Skip to content

Commit da90e09

Browse files
authored
Merge pull request #16 from square/laravel-casts
Proposal for integration with Laravel casts
2 parents 82fc626 + e9edabf commit da90e09

File tree

12 files changed

+260
-3
lines changed

12 files changed

+260
-3
lines changed

.github/workflows/php-8-0.yml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
uses: shivammathur/setup-php@v2
2222
with:
2323
php-version: '8.0'
24+
extensions: pdo, pdo_sqlite
2425

2526
- name: Validate composer.json and composer.lock
2627
run: composer validate --strict

.github/workflows/php-8-1.yml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
uses: shivammathur/setup-php@v2
2222
with:
2323
php-version: '8.1'
24+
extensions: pdo, pdo_sqlite
2425

2526
- name: Validate composer.json and composer.lock
2627
run: composer validate --strict

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
ci-php80:
2-
./vendor/bin/phpcs --standard=PSR2 src/ tests/
32
./vendor/bin/phpunit --testsuite php80
3+
./vendor/bin/phpcs --standard=PSR2 src/ tests/
44
vendor/bin/phpstan analyse src tests --level 5 -c phpstan.php80.neon
55

66
ci-php81:
7-
./vendor/bin/phpcs --standard=PSR2 src/ tests/
87
./vendor/bin/phpunit --testsuite php81
8+
./vendor/bin/phpcs --standard=PSR2 src/ tests/
99
vendor/bin/phpstan analyse src tests --level 5 -c phpstan.neon

README.md

+73
Original file line numberDiff line numberDiff line change
@@ -481,3 +481,76 @@ that this is expected behavior by adding this library's extension in your `phpst
481481
includes:
482482
- vendor/square/pjson/extension.neon
483483
```
484+
485+
## Laravel Integration
486+
487+
### via castable
488+
489+
If you wish to cast Eloquent model attributes to classes via Pjson, you might do so with the provided casting utilities:
490+
491+
```php
492+
use Illuminate\Contracts\Database\Eloquent\Castable;
493+
use Square\Pjson\Json;
494+
use Square\Pjson\JsonSerialize;
495+
use Square\Pjson\Integrations\Laravel\JsonCastable;
496+
497+
class Schedule implements Castable // implement the laravel interface
498+
{
499+
use JsonSerialize;
500+
use JsonCastable; // use the provided Pjson trait
501+
502+
#[Json]
503+
protected int $start;
504+
505+
#[Json]
506+
protected int $end;
507+
508+
public function __construct(int $start, int $end)
509+
{
510+
$this->start = $start;
511+
$this->end = $end;
512+
}
513+
}
514+
```
515+
516+
Then in your Eloquent model:
517+
518+
```php
519+
$casts = [
520+
'schedule' => Schedule::class,
521+
];
522+
```
523+
524+
### via cast arguments
525+
526+
Alternatively, you can simply use Laravel's cast arguments. In this case the `Schedule` class stays the way it used to be:
527+
528+
```php
529+
use Square\Pjson\Json;
530+
use Square\Pjson\JsonSerialize;
531+
532+
class Schedule
533+
{
534+
use JsonSerialize;
535+
536+
#[Json]
537+
protected int $start;
538+
539+
#[Json]
540+
protected int $end;
541+
542+
public function __construct(int $start, int $end)
543+
{
544+
$this->start = $start;
545+
$this->end = $end;
546+
}
547+
}
548+
```
549+
550+
And you provide the class target of the cast like:
551+
552+
```php
553+
$casts = [
554+
'schedule' => JsonCaster::class.':'.Schedule::class,
555+
];
556+
```

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"symfony/var-dumper": "^6.0",
2323
"phpunit/phpunit": "^9.5",
2424
"squizlabs/php_codesniffer": "^3.7",
25-
"phpstan/phpstan": "^1.8"
25+
"phpstan/phpstan": "^1.8",
26+
"orchestra/testbench": "^7.11"
2627
}
2728
}

phpunit.xml

+3
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@
1919
<directory suffix="Test.php">tests</directory>
2020
</testsuite>
2121
</testsuites>
22+
<php>
23+
<env name="DB_CONNECTION" value="testing"/>
24+
</php>
2225
</phpunit>
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Square\Pjson\Integrations\Laravel;
4+
5+
trait JsonCastable
6+
{
7+
public static function castUsing(array $arguments)
8+
{
9+
return new JsonCaster(static::class);
10+
}
11+
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Square\Pjson\Integrations\Laravel;
4+
5+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6+
7+
class JsonCaster implements CastsAttributes
8+
{
9+
public function __construct(
10+
protected string $target
11+
) {
12+
}
13+
14+
public function get($model, $key, $value, $attributes)
15+
{
16+
if ($value === null || $value === '') {
17+
return $value;
18+
}
19+
20+
return $this->target::fromJsonString($value);
21+
}
22+
23+
public function set($model, $key, $value, $attributes)
24+
{
25+
return $value?->toJson();
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Square\Pjson\Tests\Definitions\Integrations\Laravel;
4+
5+
use Square\Pjson\Json;
6+
use Square\Pjson\JsonSerialize;
7+
8+
class Address
9+
{
10+
use JsonSerialize;
11+
12+
#[Json]
13+
protected string $line1;
14+
#[Json]
15+
protected string $line2;
16+
#[Json]
17+
protected string $city;
18+
#[Json]
19+
protected string $zipcode;
20+
#[Json]
21+
protected string $country;
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Square\Pjson\Tests\Definitions\Integrations\Laravel;
4+
5+
use Illuminate\Contracts\Database\Eloquent\Castable;
6+
use Square\Pjson\Integrations\Laravel\JsonCastable;
7+
8+
class CastableAddress extends Address implements Castable
9+
{
10+
use JsonCastable;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Square\Pjson\Tests\Definitions\Integrations\Laravel\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Square\Pjson\Integrations\Laravel\JsonCaster;
7+
use Square\Pjson\Tests\Definitions\Integrations\Laravel\Address;
8+
use Square\Pjson\Tests\Definitions\Integrations\Laravel\CastableAddress;
9+
10+
class FirstModel extends Model
11+
{
12+
protected $table = 'first_model';
13+
protected $guarded = [];
14+
public $timestamps = false;
15+
16+
protected $casts = [
17+
// Using laravel's castable interface
18+
'castable_address' => CastableAddress::class,
19+
// Using cast attributes
20+
'address' => JsonCaster::class.':'.Address::class,
21+
];
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types=1);
2+
namespace Square\Pjson\Tests\Integrations\Laravel;
3+
4+
use Illuminate\Support\Facades\Schema;
5+
use Orchestra\Testbench\TestCase;
6+
use Square\Pjson\Tests\Definitions\Integrations\Laravel\Address;
7+
use Square\Pjson\Tests\Definitions\Integrations\Laravel\CastableAddress;
8+
use Square\Pjson\Tests\Definitions\Integrations\Laravel\Models\FirstModel;
9+
10+
class LaravelCastTest extends TestCase
11+
{
12+
public function setUp(): void
13+
{
14+
parent::setUp();
15+
Schema::create('first_model', function ($table) {
16+
$table->increments('id')->unsigned();
17+
$table->string('name')->nullable();
18+
$table->text('castable_address')->nullable();
19+
$table->text('address')->nullable();
20+
});
21+
}
22+
23+
public function testCasts()
24+
{
25+
$addr = '{"line1":"678 Lombard St.","line2":"","city":"San Fransokyo","zipcode":"94959","country":"USA"}';
26+
(new FirstModel([
27+
'name' => 'jane',
28+
'castable_address' => CastableAddress::fromJsonString($addr),
29+
'address' => Address::fromJsonString($addr),
30+
]))->save();
31+
$m = FirstModel::query()->first();
32+
$dbAddr = $m->getAttributes()['castable_address'];
33+
$this->assertEquals($addr, $dbAddr);
34+
$dbAddr = $m->getAttributes()['address'];
35+
$this->assertEquals($addr, $dbAddr);
36+
37+
// Reading the attribute directly should give objects of the right class
38+
$caddr = $m->castable_address;
39+
$this->assertEquals(CastableAddress::class, get_class($caddr));
40+
$this->assertEquals($addr, $caddr->toJson());
41+
$oaddr = $m->address;
42+
$this->assertEquals(Address::class, get_class($oaddr));
43+
$this->assertEquals($addr, $oaddr->toJson());
44+
}
45+
46+
public function testNullCase()
47+
{
48+
(new FirstModel([
49+
'name' => 'jane',
50+
'castable_address' => null,
51+
'address' => null,
52+
]))->save();
53+
$m = FirstModel::query()->first();
54+
$this->assertTrue(null === $m->getAttributes()['castable_address']);
55+
$this->assertTrue(null === $m->castable_address);
56+
$this->assertTrue(null === $m->getAttributes()['address']);
57+
$this->assertTrue(null === $m->address);
58+
}
59+
60+
public function testNotSet()
61+
{
62+
(new FirstModel([
63+
'name' => 'jane',
64+
]))->save();
65+
$m = FirstModel::query()->first();
66+
$this->assertTrue(null === $m->getAttributes()['castable_address']);
67+
$this->assertTrue(null === $m->castable_address);
68+
$this->assertTrue(null === $m->getAttributes()['address']);
69+
$this->assertTrue(null === $m->address);
70+
}
71+
72+
public function testEmptyString()
73+
{
74+
FirstModel::query()->insert([
75+
'name' => 'jane',
76+
'castable_address' => '',
77+
'address' => '',
78+
]);
79+
$m = FirstModel::query()->first();
80+
$this->assertTrue('' === $m->getAttributes()['castable_address']);
81+
$this->assertTrue('' === $m->castable_address);
82+
$this->assertTrue('' === $m->getAttributes()['address']);
83+
$this->assertTrue('' === $m->address);
84+
}
85+
}

0 commit comments

Comments
 (0)