-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathActiveRecord.php
More file actions
264 lines (245 loc) · 8.65 KB
/
Copy pathActiveRecord.php
File metadata and controls
264 lines (245 loc) · 8.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
<?php
declare(strict_types=1);
namespace Cycle\ActiveRecord;
use Cycle\ActiveRecord\Exception\Transaction\TransactionException;
use Cycle\ActiveRecord\Internal\TransactionFacade;
use Cycle\ActiveRecord\Query\ActiveQuery;
use Cycle\Database\DatabaseInterface;
use Cycle\ORM\EntityManagerInterface;
use Cycle\ORM\Exception\RunnerException;
use Cycle\ORM\ORMInterface;
use Cycle\ORM\RepositoryInterface;
use Cycle\ORM\SchemaInterface;
/**
* A base class for entities that are managed by the ORM.
* Adds a set of ActiveRecord methods to the extending entity class.
*/
abstract class ActiveRecord
{
/**
* Get the table name associated with the entity.
*
* @return non-empty-string
*/
final public static function tableName(): string
{
return static::getOrm()->getSchema()->define(static::class, SchemaInterface::TABLE);
}
/**
* Create a new entity instance with the given data.
* It is preferable to use this method instead of the constructor because
* it uses ORM services to create the entity.
*
* @note Equals to calling {@see ORMInterface::make()}.
*
* Example:
*
* ```php
* $user = User::make([
* 'name' => 'John Doe',
* 'email' => 'johndoe@example.com',
* ]);
* ```
*
* @param array<non-empty-string, mixed> $data An associative array where keys are property names
* and values are property values.
*/
public static function make(array $data): static
{
return self::getOrm()->make(static::class, $data);
}
/**
* Find a single record based on the given primary key.
*/
final public static function findByPK(mixed $primaryKey): ?static
{
return static::query()->wherePK($primaryKey)->fetchOne();
}
/**
* Find the first single record based on the given scope.
*
* @note Limit of 1 will be added to the query.
*/
final public static function findOne(array $scope = []): ?static
{
return static::query()->fetchOne($scope);
}
/**
* Find all records based on the given scope.
*
* @return iterable<static>
*/
final public static function findAll(array $scope = []): iterable
{
return static::query()->where($scope)->fetchAll();
}
/**
* Execute a callback within a single transaction.
*
* Collects all the ActiveRecord operations within the callback and executes them
* in a single {@see EntityManagerInterface} at the end of the callback.
*
* ```
* ActiveRecord::groupActions(static function (EntityManagerInterface $em) use ($user, $post): void {
* $user->saveOrFail();
* $post->saveOrFail();
* });
* ```
*
* @note DBAL operations will not be collected and executed automatically
* within the EM transaction. Use {@see self::transact()} if you need to
* execute QueryBuilder and other DBAL operations within the same transaction.
*
* @note The difference between this method and {@see self::transact()} is that
* this method opens a new transaction only when all the operations were collected
* and are ready to be executed, while {@see self::transact()} opens a transaction
* immediately when the callback is called.
*
* @note Nested calls to this method will use separated Unit of Works, but transactions
* will be reused according to the given mode.
*
* @template TResult
* @param callable(EntityManagerInterface): TResult $callback
* @return TResult
*
* @throws TransactionException
* @throws RunnerException
* @throws \Throwable
*/
final public static function groupActions(
callable $callback,
TransactionMode $mode = TransactionMode::OpenNew,
): mixed {
return TransactionFacade::groupOrmActions($callback, $mode);
}
/**
* Open a new DB transaction and execute the callback within it.
*
* All the DBAL operations within the callback will be executed within a single transaction.
* If an exception is thrown within the callback, the transaction will be rolled back.
* If the callback returns a value, the transaction will be committed.
*
* All the ORM operations within the callback will be executed in the opened transaction without collecting.
* If you need to collect ORM operations and execute them in a separated inner transaction,
* use {@see self::groupActions()} within the callback.
*
* ```
* ActiveRecord::transact(function (DatabaseInterface $db, EntityManagerInterface $em) use ($service): void {
* $user = User::query()->forUpdate()->wherePK(1)->fetchOne();
* $service->process($user);
* // ORM action will be executed right away
* $user->save();
*
* // DBAL action
* $db->getDriver()->execute('UPDATE some_table SET some_field = ? WHERE id = ?', ['value', 123]);
*
* // EM executes action right away, you don't need to call $em->run()
* $em->persist(new Post('Title', 'Content'));
* });
* ```
*
* @note If you call this method from a child class, the child database connection will be used for
* the transaction. If you call this method from the `ActiveRecord` class, the default database connection
* will be used.
*
* @template TResult
* @param callable(): TResult $callback A function that may accept parameters of the following types in any order:
* - {@see DatabaseInterface}
* - {@see EntityManagerInterface}
* - {@see ORMInterface}
* - {@see SchemaInterface}
* @psalm-param callable(...): TResult $callback
* @note that the provided Entity Manager doesn't collect operations and executes them right away in the opened transaction.
*
* @return TResult
*
* @throws TransactionException
* @throws \Throwable
*/
final public static function transact(
callable $callback,
): mixed {
return TransactionFacade::transact(
$callback,
self::getOrm()->getSchema()->defines(static::class) ? static::class : null,
);
}
/**
* Get an ActiveQuery instance for the entity.
*
* @return ActiveQuery<static>
*/
public static function query(): ActiveQuery
{
return new ActiveQuery(static::class);
}
/**
* Get the repository for the entity.
*
* @return RepositoryInterface<static>
*/
public static function getRepository(): RepositoryInterface
{
return self::getOrm()->getRepository(static::class);
}
/**
* Persist the entity.
*/
final public function save(bool $cascade = true): bool
{
$transacting = TransactionFacade::getEntityManager();
if ($transacting === null) {
return TransactionFacade::createEntityManager(TransactionMode::Ignore)
->persist($this, $cascade)
->run()
->isSuccess();
}
$transacting->persist($this, $cascade);
return true;
}
/**
* Persist the entity and throw an exception if an error occurs.
* If the method is called inside a {@see self::groupActions()}, the exception WILL NOT be thrown.
*
* @throws \Throwable
*/
final public function saveOrFail(bool $cascade = true): void
{
TransactionFacade::getEntityManager()
?->persist($this, $cascade) ?? TransactionFacade::createEntityManager(TransactionMode::Ignore)
->persist($this, $cascade)
->run();
}
/**
* Delete the entity.
*/
final public function delete(bool $cascade = true): bool
{
$transacting = TransactionFacade::getEntityManager();
if ($transacting === null) {
return TransactionFacade::createEntityManager(TransactionMode::Ignore)
->delete($this, $cascade)
->run()
->isSuccess();
}
$transacting->delete($this, $cascade);
return true;
}
/**
* Delete the entity and throw an exception if an error occurs.
* If the method is called inside a {@see self::groupActions()}, the exception WILL NOT be thrown.
*
* @throws \Throwable
*/
final public function deleteOrFail(bool $cascade = true): void
{
TransactionFacade::getEntityManager()
?->delete($this, $cascade) ?? TransactionFacade::createEntityManager(TransactionMode::Ignore)
->delete($this, $cascade)
->run();
}
private static function getOrm(): ORMInterface
{
return Facade::getOrm();
}
}