Skip to content

Commit 4770e96

Browse files
evgeekevgeny
and
evgeny
authored
Option "Saving via Eloquent Model" (#21)
* fix phpunit config deprectaion * added model param to config and MessageData * Separate savers * Move upsert/delete logic from Upserter; Implements Eloquent Saver * Added tests to MessageDataRetrieverTest * Delete Destroyer and Upserter; Reorganize structure * Tests for upsert&destroy via non-unique target keys * Tests for upsert&destroy with small chunk * optimize --------- Co-authored-by: evgeny <[email protected]>
1 parent 869b0f7 commit 4770e96

20 files changed

+602
-303
lines changed

phpunit.xml.dist

+16-14
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3-
bootstrap="vendor/autoload.php"
4-
colors="true" processIsolation="false"
5-
stopOnFailure="false"
6-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
7-
cacheDirectory=".phpunit.cache"
2+
<phpunit
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
processIsolation="false"
7+
stopOnFailure="false"
8+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
9+
cacheDirectory=".phpunit.cache"
810
>
9-
<coverage>
10-
<include>
11-
<directory suffix=".php">./src</directory>
12-
</include>
13-
<exclude>
14-
<directory suffix=".php">./database</directory>
15-
</exclude>
16-
</coverage>
1711
<php>
1812
<env name="APP_ENV" value="testing"/>
1913
<ini name="error_reporting" value="-1"/>
@@ -29,4 +23,12 @@
2923
<directory suffix="Test.php">./tests</directory>
3024
</testsuite>
3125
</testsuites>
26+
<source>
27+
<include>
28+
<directory suffix=".php">./src</directory>
29+
</include>
30+
<exclude>
31+
<directory suffix=".php">./database</directory>
32+
</exclude>
33+
</source>
3234
</phpunit>

src/Integration/Laravel/Receive/Destroyer.php

-28
This file was deleted.

src/Integration/Laravel/Receive/MessageData/MessageData.php

+30-4
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44

55
namespace Umbrellio\TableSync\Integration\Laravel\Receive\MessageData;
66

7+
use Illuminate\Database\Eloquent\Model;
8+
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\Saver;
9+
710
class MessageData
811
{
912
public function __construct(
10-
private readonly string $table,
13+
private readonly string $target,
14+
private readonly Saver $saver,
1115
private readonly array $targetKeys,
12-
private readonly array $data
16+
private readonly array $data,
1317
) {
1418
}
1519

16-
public function getTable(): string
20+
/** @return non-empty-string|class-string<Model> */
21+
public function getTarget(): string
1722
{
18-
return $this->table;
23+
return $this->target;
1924
}
2025

2126
public function getTargetKeys(): array
@@ -27,4 +32,25 @@ public function getData(): array
2732
{
2833
return $this->data;
2934
}
35+
36+
public function upsert(float $version): void
37+
{
38+
if ($this->isEmpty()) {
39+
return;
40+
}
41+
$this->saver->upsert($this, $version);
42+
}
43+
44+
public function destroy(): void
45+
{
46+
if ($this->isEmpty()) {
47+
return;
48+
}
49+
$this->saver->destroy($this);
50+
}
51+
52+
private function isEmpty(): bool
53+
{
54+
return empty(array_filter($this->data));
55+
}
3056
}

src/Integration/Laravel/Receive/MessageData/MessageDataRetriever.php

+22-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
namespace Umbrellio\TableSync\Integration\Laravel\Receive\MessageData;
66

7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Support\Facades\App;
79
use Umbrellio\TableSync\Integration\Laravel\Exceptions\Receive\IncorrectAdditionalDataHandler;
810
use Umbrellio\TableSync\Integration\Laravel\Exceptions\Receive\IncorrectConfiguration;
11+
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\EloquentSaver;
12+
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\QuerySaver;
13+
use Umbrellio\TableSync\Integration\Laravel\Receive\Savers\Saver;
914
use Umbrellio\TableSync\Messages\ReceivedMessage;
1015

1116
class MessageDataRetriever
@@ -19,11 +24,11 @@ public function __construct(
1924
public function retrieve(ReceivedMessage $message): MessageData
2025
{
2126
$messageConfig = $this->configForMessage($message);
22-
$table = $this->retrieveTable($messageConfig);
27+
[$target, $saver] = $this->retrieveTargetAndSaver($messageConfig);
2328
$targetKeys = $this->retrieveTargetKeys($messageConfig);
2429
$data = $this->retrieveData($message, $messageConfig);
2530

26-
return new MessageData($table, $targetKeys, $data);
31+
return new MessageData($target, $saver, $targetKeys, $data);
2732
}
2833

2934
private function configForMessage(ReceivedMessage $message): array
@@ -35,13 +40,24 @@ private function configForMessage(ReceivedMessage $message): array
3540
return $this->config[$message->getModel()];
3641
}
3742

38-
private function retrieveTable(array $messageConfig): string
43+
/** @return array{0: string, 1: Saver} */
44+
private function retrieveTargetAndSaver(array $messageConfig): array
3945
{
40-
if (!isset($messageConfig['table'])) {
41-
throw new IncorrectConfiguration('Table configuration required');
46+
$table = $messageConfig['table'] ?? null;
47+
$model = $messageConfig['model'] ?? null;
48+
if (!$table && !$model) {
49+
throw new IncorrectConfiguration('Table or Model configuration required');
50+
}
51+
if ($table && $model) {
52+
throw new IncorrectConfiguration('Table and Model configuration cannot be set simultaneously');
53+
}
54+
if ($model && !is_subclass_of($model, Model::class)) {
55+
throw new IncorrectConfiguration('Model must be subclass of ' . Model::class);
4256
}
4357

44-
return $messageConfig['table'];
58+
return $table ?
59+
[$table, App::make(QuerySaver::class)] :
60+
[$model, App::make(EloquentSaver::class)];
4561
}
4662

4763
private function retrieveTargetKeys(array $messageConfig): array

src/Integration/Laravel/Receive/Receiver.php

+2-6
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55
namespace Umbrellio\TableSync\Integration\Laravel\Receive;
66

7-
use Illuminate\Support\Facades\App;
87
use Umbrellio\TableSync\Integration\Laravel\Exceptions\UnknownMessageEvent;
98
use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageDataRetriever;
10-
use Umbrellio\TableSync\Integration\Laravel\Receive\Upserter\Upserter;
119
use Umbrellio\TableSync\Messages\ReceivedMessage;
1210

1311
class Receiver
@@ -24,12 +22,10 @@ public function receive(ReceivedMessage $message): void
2422

2523
switch ($event) {
2624
case 'update':
27-
$upserter = App::make(Upserter::class);
28-
$upserter->upsert($data, $message->getVersion());
25+
$data->upsert($message->getVersion());
2926
break;
3027
case 'destroy':
31-
$destroyer = new Destroyer();
32-
$destroyer->destroy($data);
28+
$data->destroy();
3329
break;
3430
default:
3531
throw new UnknownMessageEvent("Unknown event: {$event}");

src/Integration/Laravel/Receive/Upserter/ByTargetKeysResolver.php renamed to src/Integration/Laravel/Receive/Savers/ConflictResolvers/ByTargetKeysResolver.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Umbrellio\TableSync\Integration\Laravel\Receive\Upserter;
5+
namespace Umbrellio\TableSync\Integration\Laravel\Receive\Savers\ConflictResolvers;
66

77
use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageData;
88

src/Integration/Laravel/Receive/Upserter/ConflictConditionResolverContract.php renamed to src/Integration/Laravel/Receive/Savers/ConflictResolvers/ConflictConditionResolverContract.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Umbrellio\TableSync\Integration\Laravel\Receive\Upserter;
5+
namespace Umbrellio\TableSync\Integration\Laravel\Receive\Savers\ConflictResolvers;
66

77
use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageData;
88

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Umbrellio\TableSync\Integration\Laravel\Receive\Savers;
6+
7+
use Illuminate\Database\Eloquent\Builder;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Support\Arr;
10+
use Illuminate\Support\Facades\Config;
11+
use Umbrellio\TableSync\Integration\Laravel\Receive\MessageData\MessageData;
12+
13+
class EloquentSaver implements Saver
14+
{
15+
private const DEFAULT_LIMIT = 500;
16+
17+
public function upsert(MessageData $messageData, float $version): void
18+
{
19+
foreach ($messageData->getData() as $item) {
20+
$query = $this->getQueryByTargetKeys($messageData, $item);
21+
22+
if ($query->count() === 0) {
23+
$model = new ($messageData->getTarget())();
24+
$this->fillAndSaveModel($model, $version, array_keys($item), $item);
25+
continue;
26+
}
27+
28+
$this->updateChanged($query, $version, $messageData, $item);
29+
}
30+
}
31+
32+
public function destroy(MessageData $messageData): void
33+
{
34+
foreach ($messageData->getData() as $item) {
35+
$query = $this
36+
->getQueryByTargetKeys($messageData, $item)
37+
->limit($this->getLimit());
38+
39+
while ($query->count() !== 0) {
40+
$query
41+
->get()
42+
->each(fn (Model $model) => $model->forceDelete());
43+
}
44+
}
45+
}
46+
47+
protected function getQueryByTargetKeys(MessageData $messageData, array $item): Builder
48+
{
49+
/** @var class-string<Model> $modelClass */
50+
$modelClass = $messageData->getTarget();
51+
52+
return $modelClass::query()->where(Arr::only($item, $messageData->getTargetKeys()));
53+
}
54+
55+
protected function fillAndSaveModel(Model $model, float $version, array $columns, array $values): void
56+
{
57+
foreach ($columns as $key) {
58+
$model->{$key} = $values[$key];
59+
}
60+
$model->setAttribute('version', $version);
61+
$model->save();
62+
}
63+
64+
protected function updateChanged(Builder $query, float $version, MessageData $messageData, array $item): void
65+
{
66+
$columns = array_keys($messageData->getData()[0]);
67+
$updateColumns = array_diff($columns, $messageData->getTargetKeys());
68+
69+
$query->where('version', '<', $version)
70+
->where(function (Builder $builder) use ($updateColumns, $item) {
71+
foreach ($updateColumns as $column) {
72+
$builder->orWhere($column, '!=', $item[$column]);
73+
}
74+
})
75+
->limit($this->getLimit());
76+
77+
while ($query->count() !== 0) {
78+
$query
79+
->get()
80+
->each(fn (Model $model) => $this->fillAndSaveModel($model, $version, $updateColumns, $item));
81+
}
82+
}
83+
84+
protected function getLimit(): int
85+
{
86+
return Config::get('table_sync.receive.eloquent_chunk_size', self::DEFAULT_LIMIT);
87+
}
88+
}

0 commit comments

Comments
 (0)