Skip to content

Commit

Permalink
Merge pull request #464: Add JSON typecast
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk authored May 9, 2024
2 parents 892c275 + 37d98de commit 0677d29
Show file tree
Hide file tree
Showing 12 changed files with 465 additions and 33 deletions.
29 changes: 26 additions & 3 deletions src/Parser/Typecast.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
use ReflectionEnum;
use Throwable;

final class Typecast implements CastableInterface
final class Typecast implements CastableInterface, UncastableInterface
{
private const RULES = ['int', 'bool', 'float', 'datetime'];
private const RULES = ['int', 'bool', 'float', 'datetime', 'json'];

/** @var array<non-empty-string, bool> */
private array $callableRules = [];
Expand All @@ -25,7 +25,7 @@ final class Typecast implements CastableInterface
private array $rules = [];

public function __construct(
private DatabaseInterface $database
private DatabaseInterface $database,
) {
}

Expand Down Expand Up @@ -91,6 +91,28 @@ public function cast(array $data): array
return $data;
}

/**
* @throws \JsonException
*/
public function uncast(array $data): array
{
foreach ($this->rules as $column => $rule) {
if (!isset($data[$column])) {
continue;
}

$data[$column] = match ($rule) {
'json' => \json_encode(
$data[$column],
\JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE
),
default => $data[$column]
};
}

return $data;
}

/**
* @throws \Exception
*/
Expand All @@ -104,6 +126,7 @@ private function castPrimitive(mixed $rule, mixed $value): mixed
$value,
$this->database->getDriver()->getTimezone()
),
'json' => \json_decode($value, true, 512, \JSON_THROW_ON_ERROR),
default => $value,
};
}
Expand Down
16 changes: 16 additions & 0 deletions tests/ORM/Fixtures/JsonSerializableClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Fixtures;

final class JsonSerializableClass implements \JsonSerializable
{
public function jsonSerialize(): array
{
return [
'foo' => 'Lorem',
'bar' => 'Ipsum',
];
}
}
6 changes: 5 additions & 1 deletion tests/ORM/Fixtures/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ class User implements ImagedInterface
*/
public $credentials;

public ?string $settings = null;
public array $settings;

public ?array $settingsNullable = null;

public ?JsonSerializableClass $jsonSerializable = null;

public function __construct()
{
Expand Down
50 changes: 25 additions & 25 deletions tests/ORM/Functional/Driver/Common/Select/JsonMethodsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function setUp(): void
Schema::PRIMARY_KEY => 'id',
Schema::COLUMNS => ['id' => 'id', 'settings' => 'user_settings'],
Schema::SCHEMA => [],
Schema::TYPECAST => ['id' => 'int'],
Schema::TYPECAST => ['id' => 'int', 'settings' => 'json'],
Schema::RELATIONS => [],
],
Post::class => [
Expand Down Expand Up @@ -72,7 +72,7 @@ public function testWhereJson(): void
$user = $selector->whereJson('settings->theme', 'light')->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'light'], $user->settings);
}

public function testWhereJsonWithRelation(): void
Expand All @@ -81,7 +81,7 @@ public function testWhereJsonWithRelation(): void
$post = $selector->whereJson('user.settings->theme', 'light')->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'light'], $post->user->settings);
}

public function testOrWhereJson(): void
Expand All @@ -93,7 +93,7 @@ public function testOrWhereJson(): void
->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'light'], $user->settings);
}

public function testOrWhereJsonWithRelation(): void
Expand All @@ -105,7 +105,7 @@ public function testOrWhereJsonWithRelation(): void
->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'light'], $post->user->settings);
}

public function testWhereJsonContains(): void
Expand All @@ -114,7 +114,7 @@ public function testWhereJsonContains(): void
$users = $selector->whereJsonContains('settings->foo', ['bar', 'baz'])->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $users[0]->settings);
}

public function testWhereJsonContainsWithRelation(): void
Expand All @@ -123,7 +123,7 @@ public function testWhereJsonContainsWithRelation(): void
$posts = $selector->whereJsonContains('user.settings->foo', ['bar', 'baz'])->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $posts[0]->user->settings);
}

public function testOrWhereJsonContains(): void
Expand All @@ -135,7 +135,7 @@ public function testOrWhereJsonContains(): void
->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $users[0]->settings);
}

public function testOrWhereJsonContainsWithRelation(): void
Expand All @@ -147,7 +147,7 @@ public function testOrWhereJsonContainsWithRelation(): void
->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $posts[0]->user->settings);
}

public function testWhereJsonDoesntContain(): void
Expand All @@ -156,7 +156,7 @@ public function testWhereJsonDoesntContain(): void
$users = $selector->whereJsonDoesntContain('settings->theme', 'light')->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $users[0]->settings);
}

public function testWhereJsonDoesntContainWithRelation(): void
Expand All @@ -165,7 +165,7 @@ public function testWhereJsonDoesntContainWithRelation(): void
$posts = $selector->whereJsonDoesntContain('user.settings->theme', 'light')->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $posts[0]->user->settings);
}

public function testOrWhereJsonDoesntContain(): void
Expand All @@ -177,7 +177,7 @@ public function testOrWhereJsonDoesntContain(): void
->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $users[0]->settings);
}

public function testOrWhereJsonDoesntContainWithRelation(): void
Expand All @@ -189,7 +189,7 @@ public function testOrWhereJsonDoesntContainWithRelation(): void
->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $posts[0]->user->settings);
}

public function testWhereJsonContainsKey(): void
Expand All @@ -198,7 +198,7 @@ public function testWhereJsonContainsKey(): void
$user = $selector->whereJsonContainsKey('settings->foo')->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $user->settings);
}

public function testWhereJsonContainsKeyWithRelation(): void
Expand All @@ -207,7 +207,7 @@ public function testWhereJsonContainsKeyWithRelation(): void
$post = $selector->whereJsonContainsKey('user.settings->foo')->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $post->user->settings);
}

public function testOrWhereJsonContainsKey(): void
Expand All @@ -219,7 +219,7 @@ public function testOrWhereJsonContainsKey(): void
->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $user->settings);
}

public function testOrWhereJsonContainsKeyWithRelation(): void
Expand All @@ -231,7 +231,7 @@ public function testOrWhereJsonContainsKeyWithRelation(): void
->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $post->user->settings);
}

public function testWhereJsonDoesntContainKey(): void
Expand All @@ -240,7 +240,7 @@ public function testWhereJsonDoesntContainKey(): void
$user = $selector->whereJsonDoesntContainKey('settings->foo')->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'light'], $user->settings);
}

public function testWhereJsonDoesntContainKeyWithRelation(): void
Expand All @@ -249,7 +249,7 @@ public function testWhereJsonDoesntContainKeyWithRelation(): void
$post = $selector->whereJsonDoesntContainKey('user.settings->foo')->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'light'], $post->user->settings);
}

public function testOrWhereJsonDoesntContainKey(): void
Expand All @@ -261,7 +261,7 @@ public function testOrWhereJsonDoesntContainKey(): void
->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'light'], $user->settings);
}

public function testOrWhereJsonDoesntContainKeyWithRelation(): void
Expand All @@ -273,7 +273,7 @@ public function testOrWhereJsonDoesntContainKeyWithRelation(): void
->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'light'], $post->user->settings);
}

public function testWhereJsonLength(): void
Expand All @@ -282,7 +282,7 @@ public function testWhereJsonLength(): void
$user = $selector->whereJsonLength('settings->foo', 2)->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $user->settings);
}

public function testWhereJsonLengthWithRelation(): void
Expand All @@ -291,7 +291,7 @@ public function testWhereJsonLengthWithRelation(): void
$post = $selector->whereJsonLength('user.settings->foo', 2)->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $post->user->settings);
}

public function testOrWhereJsonLength(): void
Expand All @@ -303,7 +303,7 @@ public function testOrWhereJsonLength(): void
->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $user->settings);
}

public function testOrWhereJsonLengthWithRelation(): void
Expand All @@ -315,6 +315,6 @@ public function testOrWhereJsonLengthWithRelation(): void
->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], $post->user->settings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
namespace Cycle\ORM\Tests\Functional\Driver\Common\Typecast\Fixture;

use Cycle\ORM\Parser\CastableInterface;
use Cycle\ORM\Parser\UncastableInterface;

class JsonTypecast implements CastableInterface
class JsonTypecast implements CastableInterface, UncastableInterface
{
private array $rules = [];

Expand All @@ -29,7 +30,23 @@ public function cast(array $data): array
continue;
}

$data[$key] = 'json';
$data[$key] = ['json'];
}

return $data;
}

public function uncast(array $data): array
{
foreach ($this->rules as $column => $rule) {
if (!isset($data[$column])) {
continue;
}

$data[$column] = match ($rule) {
'json' => 'uncast-json',
default => (string) $data[$column]
};
}

return $data;
Expand Down
Loading

0 comments on commit 0677d29

Please sign in to comment.