Skip to content

Commit 7ae5c2e

Browse files
committed
feat: add withoutEntities() method to temporarily drop entities during a callback
1 parent b0c7456 commit 7ae5c2e

File tree

4 files changed

+259
-54
lines changed

4 files changed

+259
-54
lines changed

README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,49 @@ SqlEntity::createAll();
262262
SqlEntity::dropAll();
263263

264264
// You can also filter by type or connection
265-
SqlEntity::createAll(type: View::class, connection: 'reporting');
266-
SqlEntity::dropAll(type: View::class, connection: 'reporting');
265+
SqlEntity::createAll(types: View::class, connections: 'reporting');
266+
SqlEntity::dropAll(types: View::class, connections: 'reporting');
267267
```
268268

269+
#### ♻️ `withoutEntities()`
270+
271+
Sometimes you need to run a block of logic (like renaming a table column) *without certain SQL entities present*.
272+
The `withoutEntities()` method temporarily drops the selected entities, executes your callback, and then recreates them afterward.
273+
274+
If the database connection supports **schema transactions**, the entire operation is wrapped in one.
275+
276+
```php
277+
<?php
278+
use CalebDW\SqlEntities\Facades\SqlEntity;
279+
use Illuminate\Database\Connection;
280+
281+
SqlEntity::withoutEntities(function (Connection $connection) {
282+
$connection->getSchemaBuilder()->table('orders', function ($table) {
283+
$table->renameColumn('old_customer_id', 'customer_id');
284+
});
285+
});
286+
```
287+
288+
You can also restrict the scope to certain entity types or connections:
289+
290+
```php
291+
<?php
292+
use CalebDW\SqlEntities\Facades\SqlEntity;
293+
use Illuminate\Database\Connection;
294+
295+
SqlEntity::withoutEntities(
296+
callback: function (Connection $connection) {
297+
$connection->getSchemaBuilder()->table('orders', function ($table) {
298+
$table->renameColumn('old_customer_id', 'customer_id');
299+
});
300+
},
301+
types: [RecentOrdersView::class, RecentHighValueOrdersView::class],
302+
connections: ['reporting'],
303+
);
304+
```
305+
306+
After the callback, all affected entities are automatically recreated in dependency order.
307+
269308
### 🚀 Automatic syncing when migrating (Optional)
270309

271310
You may want to automatically drop all SQL entities before migrating, and then

src/Facades/SqlEntity.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66

77
use CalebDW\SqlEntities\Contracts\SqlEntity as SqlEntityContract;
88
use CalebDW\SqlEntities\SqlEntityManager;
9+
use Closure;
10+
use Illuminate\Database\Connection;
911
use Illuminate\Support\Facades\Facade;
1012
use Override;
1113

1214
/**
1315
* @method static SqlEntityContract get(string $name)
14-
* @method static void create(SqlEntityContract|class-string<SqlEntityContract>|string $entity)
15-
* @method static void drop(SqlEntityContract|class-string<SqlEntityContract>|string $entity)
16-
* @method static void createAll(?string $type = null, ?string $connection = null)
17-
* @method static void dropAll(?string $type = null, ?string $connection = null)
16+
* @method static void create(SqlEntityContract|class-string<SqlEntityContract> $entity)
17+
* @method static void drop(SqlEntityContract|class-string<SqlEntityContract> $entity)
18+
* @method static void createAll(array<int, class-string<SqlEntityContract>>|class-string<SqlEntityContract>|null $types = null, array<int, string>|string|null $connections = null)
19+
* @method static void dropAll(array<int, class-string<SqlEntityContract>>|class-string<SqlEntityContract>|null $types = null, array<int, string>|string|null $connections = null)
20+
* @method static void withoutEntities(Closure(Connection): mixed $callback, array<int, class-string<SqlEntityContract>>|class-string<SqlEntityContract>|null $types = null, array<int, string>|string|null $connections = null)
1821
*
1922
* @see SqlEntityManager
2023
*/
@@ -23,6 +26,6 @@ class SqlEntity extends Facade
2326
#[Override]
2427
protected static function getFacadeAccessor(): string
2528
{
26-
return SqlEntityManager::class;
29+
return 'sql-entities';
2730
}
2831
}

src/SqlEntityManager.php

Lines changed: 135 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,29 @@
1212
use CalebDW\SqlEntities\Grammars\PostgresGrammar;
1313
use CalebDW\SqlEntities\Grammars\SQLiteGrammar;
1414
use CalebDW\SqlEntities\Grammars\SqlServerGrammar;
15+
use Closure;
1516
use Illuminate\Database\Connection;
1617
use Illuminate\Database\DatabaseManager;
18+
use Illuminate\Support\Arr;
1719
use Illuminate\Support\Collection;
1820
use Illuminate\Support\ItemNotFoundException;
1921
use InvalidArgumentException;
2022

23+
/**
24+
* @phpstan-type TEntities Collection<class-string<SqlEntity>, SqlEntity>
25+
*/
2126
class SqlEntityManager
2227
{
2328
use SortsTopologically;
2429

25-
/** @var Collection<class-string<SqlEntity>, SqlEntity> */
30+
/**
31+
* The active connection instances.
32+
*
33+
* @var array<string, Connection>
34+
*/
35+
protected array $connections = [];
36+
37+
/** @var TEntities */
2638
public Collection $entities;
2739

2840
/**
@@ -32,7 +44,7 @@ class SqlEntityManager
3244
*/
3345
protected array $grammars = [];
3446

35-
/** @param Collection<int, SqlEntity> $entities */
47+
/** @param Collection<array-key, SqlEntity> $entities */
3648
public function __construct(
3749
Collection $entities,
3850
protected DatabaseManager $db,
@@ -51,15 +63,15 @@ public function __construct(
5163
/**
5264
* Get the entity by class.
5365
*
54-
* @param class-string<SqlEntity> $name
66+
* @param class-string<SqlEntity> $class
5567
* @throws ItemNotFoundException
5668
*/
57-
public function get(string $name): SqlEntity
69+
public function get(string $class): SqlEntity
5870
{
59-
$entity = $this->entities->get($name);
71+
$entity = $this->entities->get($class);
6072

6173
if ($entity === null) {
62-
throw new ItemNotFoundException("Entity [{$name}] not found.");
74+
throw new ItemNotFoundException("Entity [{$class}] not found.");
6375
}
6476

6577
return $entity;
@@ -77,7 +89,7 @@ public function create(SqlEntity|string $entity): void
7789
$entity = $this->get($entity);
7890
}
7991

80-
$connection = $this->connection($entity);
92+
$connection = $this->connection($entity->connectionName());
8193

8294
if (! $entity->creating($connection)) {
8395
return;
@@ -101,7 +113,7 @@ public function drop(SqlEntity|string $entity): void
101113
$entity = $this->get($entity);
102114
}
103115

104-
$connection = $this->connection($entity);
116+
$connection = $this->connection($entity->connectionName());
105117

106118
if (! $entity->dropping($connection)) {
107119
return;
@@ -113,32 +125,129 @@ public function drop(SqlEntity|string $entity): void
113125
$entity->dropped($connection);
114126
}
115127

116-
/** @param class-string<SqlEntity>|null $type */
117-
public function createAll(?string $type = null, ?string $connection = null): void
118-
{
128+
/**
129+
* Create all entities.
130+
*
131+
* @param array<int, class-string<SqlEntity>>|class-string<SqlEntity>|null $types
132+
* @param array<int, string>|string|null $connections
133+
*/
134+
public function createAll(
135+
array|string|null $types = null,
136+
array|string|null $connections = null,
137+
): void {
119138
$this->entities
120-
->when($connection, fn ($c) => $c->filter(
121-
fn ($e) => $e->connectionName() === $connection,
122-
))
123-
->when($type, fn ($c, $t) => $c->filter(fn ($e) => is_a($e, $t)))
124-
->each(fn ($e) => $this->create($e));
139+
->when($connections, $this->filterByConnections(...))
140+
->when($types, $this->filterByTypes(...))
141+
->each($this->create(...));
125142
}
126143

127-
/** @param class-string<SqlEntity>|null $type */
128-
public function dropAll(?string $type = null, ?string $connection = null): void
129-
{
144+
/**
145+
* Drop all entities.
146+
*
147+
* @param array<int, class-string<SqlEntity>>|class-string<SqlEntity>|null $types
148+
* @param array<int, string>|string|null $connections
149+
*/
150+
public function dropAll(
151+
array|string|null $types = null,
152+
array|string|null $connections = null,
153+
): void {
130154
$this->entities
131155
->reverse()
132-
->when($connection, fn ($c) => $c->filter(
133-
fn ($e) => $e->connectionName() === $connection,
134-
))
135-
->when($type, fn ($c, $t) => $c->filter(fn ($e) => is_a($e, $t)))
136-
->each(fn ($e) => $this->drop($e));
156+
->when($connections, $this->filterByConnections(...))
157+
->when($types, $this->filterByTypes(...))
158+
->each($this->drop(...));
137159
}
138160

139-
protected function connection(SqlEntity $entity): Connection
161+
/**
162+
* Execute a callback (in a transaction, if supported) without the specified entities.
163+
*
164+
* @param Closure(Connection): mixed $callback
165+
* @param array<int, class-string<SqlEntity>>|class-string<SqlEntity>|null $types
166+
* @param array<int, string>|string|null $connections
167+
*/
168+
public function withoutEntities(
169+
Closure $callback,
170+
array|string|null $types = null,
171+
array|string|null $connections = null,
172+
): void {
173+
$defaultConnection = $this->db->getDefaultConnection();
174+
175+
$groups = $this->entities
176+
->when($connections, $this->filterByConnections(...))
177+
->when($types, $this->filterByTypes(...))
178+
->groupBy(fn ($e) => $e->connectionName() ?? $defaultConnection);
179+
180+
foreach ($groups as $connectionName => $entities) {
181+
$connection = $this->connection($connectionName);
182+
183+
$execute = function () use ($connection, $entities, $callback) {
184+
$entities
185+
->reverse()
186+
->each($this->drop(...));
187+
188+
$callback($connection);
189+
190+
$entities->each($this->create(...));
191+
};
192+
193+
/** @phpstan-ignore identical.alwaysFalse (bad phpdocs) */
194+
if ($connection->getSchemaGrammar() === null) {
195+
$connection->useDefaultSchemaGrammar();
196+
}
197+
198+
$connection->getSchemaGrammar()->supportsSchemaTransactions()
199+
? $connection->transaction($execute)
200+
: $execute();
201+
}
202+
}
203+
204+
/**
205+
* Filter entities by connection.
206+
*
207+
* @param TEntities $entities
208+
* @param array<int, class-string<SqlEntity>>|class-string<SqlEntity> $types
209+
* @return TEntities
210+
*/
211+
protected function filterByTypes(
212+
Collection $entities,
213+
array|string $types,
214+
): Collection {
215+
return $entities->filter(function ($entity) use ($types) {
216+
foreach (Arr::wrap($types) as $type) {
217+
if (is_a($entity, $type, allow_string: false)) {
218+
return true;
219+
}
220+
}
221+
222+
return false;
223+
});
224+
}
225+
226+
/**
227+
* Filter entities by connection.
228+
*
229+
* @param TEntities $entities
230+
* @param array<int, string>|string $connections
231+
* @return TEntities
232+
*/
233+
protected function filterByConnections(
234+
Collection $entities,
235+
array|string $connections,
236+
): Collection {
237+
$default = $this->db->getDefaultConnection();
238+
239+
return $entities->filter(function ($entity) use ($connections, $default) {
240+
$name = $entity->connectionName() ?? $default;
241+
242+
return in_array($name, Arr::wrap($connections), strict: true);
243+
});
244+
}
245+
246+
protected function connection(?string $name): Connection
140247
{
141-
return $this->db->connection($entity->connectionName());
248+
$name ??= $this->db->getDefaultConnection();
249+
250+
return $this->connections[$name] ??= $this->db->connection($name);
142251
}
143252

144253
protected function grammar(Connection $connection): Grammar

0 commit comments

Comments
 (0)