Skip to content

Commit 0e78895

Browse files
authored
feat(database): add insert()->then() and prevent invalid relations from being attached (#1225)
1 parent a4fa4a6 commit 0e78895

File tree

16 files changed

+284
-57
lines changed

16 files changed

+284
-57
lines changed

packages/database/src/Builder/ModelInspector.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use ReflectionException;
66
use Tempest\Database\Config\DatabaseConfig;
7+
use Tempest\Database\HasOne;
78
use Tempest\Database\Table;
89
use Tempest\Reflection\ClassReflector;
910
use Tempest\Validation\Exceptions\ValidationException;
@@ -69,7 +70,7 @@ public function getPropertyValues(): array
6970
continue;
7071
}
7172

72-
if ($property->getIterableType()?->isRelation()) {
73+
if ($this->isHasManyRelation($property->getName()) || $this->isHasOneRelation($property->getName())) {
7374
continue;
7475
}
7576

@@ -81,6 +82,44 @@ public function getPropertyValues(): array
8182
return $values;
8283
}
8384

85+
public function isHasManyRelation(string $name): bool
86+
{
87+
if (! $this->isObjectModel()) {
88+
return false;
89+
}
90+
91+
if (! $this->modelClass->hasProperty($name)) {
92+
return false;
93+
}
94+
95+
$property = $this->modelClass->getProperty($name);
96+
97+
if ($property->getIterableType()?->isRelation()) {
98+
return true;
99+
}
100+
101+
return false;
102+
}
103+
104+
public function isHasOneRelation(string $name): bool
105+
{
106+
if (! $this->isObjectModel()) {
107+
return false;
108+
}
109+
110+
if (! $this->modelClass->hasProperty($name)) {
111+
return false;
112+
}
113+
114+
$property = $this->modelClass->getProperty($name);
115+
116+
if ($property->hasAttribute(HasOne::class)) {
117+
return true;
118+
}
119+
120+
return false;
121+
}
122+
84123
public function validate(mixed ...$data): void
85124
{
86125
if (! $this->isObjectModel()) {
@@ -111,4 +150,13 @@ public function validate(mixed ...$data): void
111150
throw new ValidationException($this->modelClass->getName(), $failingRules);
112151
}
113152
}
153+
154+
public function getName(): string
155+
{
156+
if ($this->isObjectModel()) {
157+
return $this->modelClass->getName();
158+
}
159+
160+
return $this->modelClass;
161+
}
114162
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Tempest\Database\Builder\QueryBuilders;
4+
5+
use Tempest\Database\Query;
6+
7+
interface BuildsQuery
8+
{
9+
public function build(mixed ...$bindings): Query;
10+
}

packages/database/src/Builder/QueryBuilders/CountQueryBuilder.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
/**
1616
* @template TModelClass of object
1717
*/
18-
final class CountQueryBuilder
18+
final class CountQueryBuilder implements BuildsQuery
1919
{
2020
use HasConditions;
2121

@@ -93,7 +93,7 @@ public function toSql(): string
9393
return $this->build()->getSql();
9494
}
9595

96-
public function build(array $bindings = []): Query
96+
public function build(mixed ...$bindings): Query
9797
{
9898
return new Query($this->count, [...$this->bindings, ...$bindings]);
9999
}

packages/database/src/Builder/QueryBuilders/DeleteQueryBuilder.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/**
1515
* @template TModelClass of object
1616
*/
17-
final class DeleteQueryBuilder
17+
final class DeleteQueryBuilder implements BuildsQuery
1818
{
1919
use HasConditions;
2020

@@ -61,8 +61,8 @@ public function bind(mixed ...$bindings): self
6161
return $this;
6262
}
6363

64-
public function build(): Query
64+
public function build(mixed ...$bindings): Query
6565
{
66-
return new Query($this->delete, $this->bindings);
66+
return new Query($this->delete, [...$this->bindings, ...$bindings]);
6767
}
6868
}

packages/database/src/Builder/QueryBuilders/InsertQueryBuilder.php

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,67 @@
22

33
namespace Tempest\Database\Builder\QueryBuilders;
44

5+
use Closure;
56
use Tempest\Database\Builder\ModelDefinition;
67
use Tempest\Database\Builder\TableDefinition;
8+
use Tempest\Database\Exceptions\CannotInsertHasManyRelation;
9+
use Tempest\Database\Exceptions\CannotInsertHasOneRelation;
710
use Tempest\Database\Id;
811
use Tempest\Database\Query;
912
use Tempest\Database\QueryStatements\InsertStatement;
1013
use Tempest\Mapper\SerializerFactory;
1114
use Tempest\Reflection\ClassReflector;
1215
use Tempest\Support\Arr\ImmutableArray;
1316

14-
final readonly class InsertQueryBuilder
17+
use function Tempest\Database\model;
18+
19+
final class InsertQueryBuilder implements BuildsQuery
1520
{
1621
private InsertStatement $insert;
1722

23+
private array $after = [];
24+
1825
public function __construct(
19-
private string|object $model,
20-
private array $rows,
21-
private SerializerFactory $serializerFactory,
26+
private readonly string|object $model,
27+
private readonly array $rows,
28+
private readonly SerializerFactory $serializerFactory,
2229
) {
2330
$this->insert = new InsertStatement($this->resolveTableDefinition());
2431
}
2532

2633
public function execute(mixed ...$bindings): Id
2734
{
28-
return $this->build()->execute(...$bindings);
35+
$id = $this->build()->execute(...$bindings);
36+
37+
foreach ($this->after as $after) {
38+
$query = $after($id);
39+
40+
if ($query instanceof BuildsQuery) {
41+
$query->build()->execute();
42+
}
43+
}
44+
45+
return $id;
2946
}
3047

31-
public function build(): Query
48+
public function build(mixed ...$bindings): Query
3249
{
33-
$bindings = [];
50+
$definition = model($this->model);
51+
52+
foreach ($this->resolveData() as $data) {
53+
foreach ($data as $key => $value) {
54+
if ($definition->isHasManyRelation($key)) {
55+
throw new CannotInsertHasManyRelation($definition->getName(), $key);
56+
}
3457

35-
foreach ($this->resolveEntries() as $entry) {
36-
$this->insert->addEntry($entry);
58+
if ($definition->isHasOneRelation($key)) {
59+
throw new CannotInsertHasOneRelation($definition->getName(), $key);
60+
}
3761

38-
foreach ($entry as $value) {
3962
$bindings[] = $value;
4063
}
64+
65+
$this->insert->addEntry($data);
4166
}
4267

4368
return new Query(
@@ -46,7 +71,14 @@ public function build(): Query
4671
);
4772
}
4873

49-
private function resolveEntries(): array
74+
public function then(Closure ...$callbacks): self
75+
{
76+
$this->after = [...$this->after, ...$callbacks];
77+
78+
return $this;
79+
}
80+
81+
private function resolveData(): array
5082
{
5183
$entries = [];
5284

@@ -59,6 +91,8 @@ private function resolveEntries(): array
5991
}
6092

6193
// The rest are model objects
94+
$definition = model($model);
95+
6296
$modelClass = new ClassReflector($model);
6397

6498
$entry = [];
@@ -69,16 +103,16 @@ private function resolveEntries(): array
69103
continue;
70104
}
71105

72-
// HasMany relations are skipped
73-
if ($property->getIterableType()?->isRelation()) {
106+
// HasMany and HasOne relations are skipped
107+
if ($definition->isHasManyRelation($property->getName()) || $definition->isHasOneRelation($property->getName())) {
74108
continue;
75109
}
76110

77111
$column = $property->getName();
78112

79113
$value = $property->getValue($model);
80114

81-
// BelongsTo and HasMany relations are included
115+
// BelongsTo and reverse HasMany relations are included
82116
if ($property->getType()->isRelation()) {
83117
$column .= '_id';
84118

@@ -93,7 +127,7 @@ private function resolveEntries(): array
93127
};
94128
}
95129

96-
// Check if value needs serialization
130+
// Check if the value needs serialization
97131
$serializer = $this->serializerFactory->forProperty($property);
98132

99133
if ($value !== null && $serializer !== null) {

packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
/**
2727
* @template TModelClass of object
2828
*/
29-
final class SelectQueryBuilder
29+
final class SelectQueryBuilder implements BuildsQuery
3030
{
3131
use HasConditions;
3232

@@ -57,7 +57,7 @@ public function __construct(string|object $model, ?ImmutableArray $columns = nul
5757
*/
5858
public function first(mixed ...$bindings): mixed
5959
{
60-
$query = $this->build($bindings);
60+
$query = $this->build(...$bindings);
6161

6262
$result = map($query)->collection()->to($this->modelClass);
6363

@@ -79,7 +79,7 @@ public function get(Id $id): mixed
7979
/** @return TModelClass[] */
8080
public function all(mixed ...$bindings): array
8181
{
82-
return map($this->build($bindings))->collection()->to($this->modelClass);
82+
return map($this->build(...$bindings))->collection()->to($this->modelClass);
8383
}
8484

8585
/**
@@ -182,7 +182,7 @@ public function toSql(): string
182182
return $this->build()->getSql();
183183
}
184184

185-
public function build(array $bindings = []): Query
185+
public function build(mixed ...$bindings): Query
186186
{
187187
$resolvedRelations = $this->resolveRelations();
188188

packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tempest\Database\Builder\QueryBuilders;
44

55
use Tempest\Database\Exceptions\CannotUpdateHasManyRelation;
6+
use Tempest\Database\Exceptions\CannotUpdateHasOneRelation;
67
use Tempest\Database\Id;
78
use Tempest\Database\Query;
89
use Tempest\Database\QueryStatements\UpdateStatement;
@@ -15,7 +16,7 @@
1516
use function Tempest\Database\model;
1617
use function Tempest\Support\arr;
1718

18-
final class UpdateQueryBuilder
19+
final class UpdateQueryBuilder implements BuildsQuery
1920
{
2021
use HasConditions;
2122

@@ -61,7 +62,7 @@ public function bind(mixed ...$bindings): self
6162
return $this;
6263
}
6364

64-
public function build(): Query
65+
public function build(mixed ...$bindings): Query
6566
{
6667
$values = $this->resolveValues();
6768

@@ -73,8 +74,6 @@ public function build(): Query
7374
$this->where('`id` = ?', id: $this->model->id->id);
7475
}
7576

76-
$bindings = [];
77-
7877
foreach ($values as $value) {
7978
$bindings[] = $value;
8079
}
@@ -88,7 +87,9 @@ public function build(): Query
8887

8988
private function resolveValues(): ImmutableArray
9089
{
91-
if (! model($this->model)->isObjectModel()) {
90+
$modelDefinition = model($this->model);
91+
92+
if (! $modelDefinition->isObjectModel()) {
9293
return arr($this->values);
9394
}
9495

@@ -99,10 +100,14 @@ private function resolveValues(): ImmutableArray
99100
foreach ($this->values as $column => $value) {
100101
$property = $modelClass->getProperty($column);
101102

102-
if ($property->getIterableType()?->isRelation()) {
103+
if ($modelDefinition->isHasManyRelation($property->getName())) {
103104
throw new CannotUpdateHasManyRelation($modelClass->getName(), $property->getName());
104105
}
105106

107+
if ($modelDefinition->isHasOneRelation($property->getName())) {
108+
throw new CannotUpdateHasOneRelation($modelClass->getName(), $property->getName());
109+
}
110+
106111
if ($property->getType()->isRelation()) {
107112
$column .= '_id';
108113

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Database\Exceptions;
4+
5+
use Exception;
6+
7+
final class CannotInsertHasOneRelation extends Exception
8+
{
9+
public function __construct(string $modelName, string $relationName)
10+
{
11+
parent::__construct("Cannot create {$modelName}::\${$relationName} via an insert query. Attach the related has many model manually instead");
12+
}
13+
}

packages/database/src/Exceptions/CannotUpdateHasManyRelation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ final class CannotUpdateHasManyRelation extends Exception
88
{
99
public function __construct(string $modelName, string $relationName)
1010
{
11-
parent::__construct("Cannot update {$modelName}::\${$relationName} via an update query. Attach the related has many model manually instead");
11+
parent::__construct("Cannot update {$modelName}::\${$relationName} via an update query. Update the related model directly instead");
1212
}
1313
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Database\Exceptions;
4+
5+
use Exception;
6+
7+
final class CannotUpdateHasOneRelation extends Exception
8+
{
9+
public function __construct(string $modelName, string $relationName)
10+
{
11+
parent::__construct("Cannot update {$modelName}::\${$relationName} via an update query. Update the related model directly instead");
12+
}
13+
}

0 commit comments

Comments
 (0)