Skip to content

Commit 49314dd

Browse files
committed
feat: add view entity and grammar support
1 parent 363eaf1 commit 49314dd

File tree

9 files changed

+431
-1
lines changed

9 files changed

+431
-1
lines changed

phpstan.neon.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
parameters:
2-
level: max
2+
level: 8
33
paths:
44
- src
55
- workbench/app

src/Entities/Entity.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CalebDW\SqlEntities\Entities;
6+
7+
use Illuminate\Database\Connection;
8+
use Illuminate\Database\Query\Builder;
9+
use Stringable;
10+
11+
abstract class Entity implements Stringable
12+
{
13+
/** The entity name. */
14+
abstract public function name(): string;
15+
16+
/** The entity definition. */
17+
abstract public function definition(): Builder|string;
18+
19+
/** The entity connection name. */
20+
public function connectionName(): ?string
21+
{
22+
return null;
23+
}
24+
25+
/** Hook before creating the entity. */
26+
public function creating(Connection $connection): void
27+
{
28+
}
29+
30+
/** Hook after creating the entity. */
31+
public function created(Connection $connection): void
32+
{
33+
}
34+
35+
/** Hook before dropping the entity. */
36+
public function dropping(Connection $connection): void
37+
{
38+
}
39+
40+
/** Hook after dropping the entity. */
41+
public function dropped(Connection $connection): void
42+
{
43+
}
44+
45+
public function toString(): string
46+
{
47+
$definition = $this->definition();
48+
49+
if (is_string($definition)) {
50+
return $definition;
51+
}
52+
53+
return $definition->toRawSql();
54+
}
55+
56+
public function __toString(): string
57+
{
58+
return $this->toString();
59+
}
60+
}

src/Entities/View.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CalebDW\SqlEntities\Entities;
6+
7+
abstract class View extends Entity
8+
{
9+
}

src/EntityManager.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CalebDW\SqlEntities;
6+
7+
use CalebDW\SqlEntities\Entities\Entity;
8+
use CalebDW\SqlEntities\Grammars\Grammar;
9+
use CalebDW\SqlEntities\Grammars\PostgresGrammar;
10+
use CalebDW\SqlEntities\Grammars\SQLiteGrammar;
11+
use Illuminate\Database\Connection;
12+
use Illuminate\Database\DatabaseManager;
13+
use Illuminate\Support\Collection;
14+
use InvalidArgumentException;
15+
16+
class EntityManager
17+
{
18+
/**
19+
* The active grammar instances.
20+
*
21+
* @var array<string, Grammar>
22+
*/
23+
protected array $grammars = [];
24+
25+
public function __construct(
26+
/** @var Collection<int, Entity> */
27+
public readonly Collection $entities,
28+
protected DatabaseManager $db,
29+
) {
30+
}
31+
32+
/** @throws InvalidArgumentException if the entity is not found. */
33+
public function get(string $name, ?string $connection = null): Entity
34+
{
35+
$entity = $this->entities->firstWhere(
36+
fn (Entity $e) => $e->name() === $name
37+
&& $e->connectionName() === $connection,
38+
);
39+
40+
throw_if(
41+
$entity === null,
42+
new InvalidArgumentException("Entity [{$name}] not found."),
43+
);
44+
45+
return $entity;
46+
}
47+
48+
/** @throws InvalidArgumentException if the entity is not found. */
49+
public function create(Entity|string $entity): void
50+
{
51+
if (is_string($entity)) {
52+
$entity = $this->get($entity);
53+
}
54+
55+
$connection = $this->connection($entity);
56+
$grammar = $this->grammar($connection);
57+
58+
$entity->creating($connection);
59+
$connection->statement($grammar->compileCreate($entity));
60+
$entity->created($connection);
61+
}
62+
63+
/** @throws InvalidArgumentException if the entity is not found. */
64+
public function drop(Entity|string $entity): void
65+
{
66+
if (is_string($entity)) {
67+
$entity = $this->get($entity);
68+
}
69+
70+
$connection = $this->connection($entity);
71+
$grammar = $this->grammar($connection);
72+
73+
$entity->dropping($connection);
74+
$connection->statement($grammar->compileDrop($entity));
75+
$entity->dropped($connection);
76+
}
77+
78+
/** @param class-string<Entity>|null $type */
79+
public function createAll(?string $type = null, ?string $connection = null): void
80+
{
81+
$this->entities
82+
->when($connection, fn ($c) => $c->filter(
83+
fn ($e) => $e->connectionName() === $connection,
84+
))
85+
->when($type, fn ($c, $t) => $c->filter(fn ($e) => is_a($e, $t)))
86+
->each(fn ($e) => $this->create($e));
87+
}
88+
89+
/** @param class-string<Entity>|null $type */
90+
public function dropAll(?string $type = null, ?string $connection = null): void
91+
{
92+
$this->entities
93+
->when($connection, fn ($c) => $c->filter(
94+
fn ($e) => $e->connectionName() === $connection,
95+
))
96+
->when($type, fn ($c, $t) => $c->filter(fn ($e) => is_a($e, $t)))
97+
->each(fn ($e) => $this->drop($e));
98+
}
99+
100+
protected function connection(Entity $entity): Connection
101+
{
102+
return $this->db->connection($entity->connectionName());
103+
}
104+
105+
protected function grammar(Connection $connection): Grammar
106+
{
107+
$driver = $connection->getDriverName();
108+
109+
return $this->grammars[$driver] ??= $this->createGrammar($driver, $connection);
110+
}
111+
112+
protected function createGrammar(string $driver, Connection $connection): Grammar
113+
{
114+
return match ($driver) {
115+
'sqlite' => new SQLiteGrammar($connection),
116+
'pgsql' => new PostgresGrammar($connection),
117+
default => throw new InvalidArgumentException(
118+
"Unsupported driver [{$driver}].",
119+
),
120+
};
121+
}
122+
}

src/Grammars/Grammar.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CalebDW\SqlEntities\Grammars;
6+
7+
use CalebDW\SqlEntities\Entities\Entity;
8+
use CalebDW\SqlEntities\Entities\View;
9+
use Illuminate\Database\Connection;
10+
use InvalidArgumentException;
11+
12+
abstract class Grammar
13+
{
14+
public function __construct(
15+
protected Connection $connection,
16+
) {
17+
}
18+
19+
public function compileCreate(Entity $entity): string
20+
{
21+
return match (true) {
22+
$entity instanceof View => $this->compileViewCreate($entity),
23+
24+
default => throw new InvalidArgumentException(
25+
sprintf('Unsupported entity [%s].', $entity::class),
26+
),
27+
};
28+
}
29+
30+
public function compileDrop(Entity $entity): string
31+
{
32+
return match (true) {
33+
$entity instanceof View => $this->compileViewDrop($entity),
34+
35+
default => throw new InvalidArgumentException(
36+
sprintf('Unsupported entity [%s].', $entity::class),
37+
),
38+
};
39+
}
40+
41+
abstract protected function compileViewCreate(View $entity): string;
42+
43+
abstract protected function compileViewDrop(View $entity): string;
44+
}

src/Grammars/PostgresGrammar.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CalebDW\SqlEntities\Grammars;
6+
7+
use CalebDW\SqlEntities\Entities\View;
8+
use Override;
9+
10+
class PostgresGrammar extends Grammar
11+
{
12+
#[Override]
13+
protected function compileViewCreate(View $entity): string
14+
{
15+
return <<<SQL
16+
CREATE OR REPLACE VIEW {$entity->name()} AS
17+
{$entity}
18+
SQL;
19+
}
20+
21+
#[Override]
22+
protected function compileViewDrop(View $entity): string
23+
{
24+
return <<<SQL
25+
DROP VIEW IF EXISTS {$entity->name()} CASCADE
26+
SQL;
27+
}
28+
}

src/Grammars/SQLiteGrammar.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CalebDW\SqlEntities\Grammars;
6+
7+
use CalebDW\SqlEntities\Entities\View;
8+
use Override;
9+
10+
class SQLiteGrammar extends Grammar
11+
{
12+
#[Override]
13+
protected function compileViewCreate(View $entity): string
14+
{
15+
return <<<SQL
16+
CREATE VIEW {$entity->name()} AS
17+
{$entity}
18+
SQL;
19+
}
20+
21+
#[Override]
22+
protected function compileViewDrop(View $entity): string
23+
{
24+
return <<<SQL
25+
DROP VIEW IF EXISTS {$entity->name()}
26+
SQL;
27+
}
28+
}

src/ServiceProvider.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CalebDW\SqlEntities;
6+
7+
use CalebDW\SqlEntities\Entities\Entity;
8+
use CalebDW\SqlEntities\Support\Composer;
9+
use Illuminate\Contracts\Foundation\Application;
10+
use Illuminate\Contracts\Support\DeferrableProvider;
11+
use Illuminate\Support\Collection;
12+
use Illuminate\Support\ServiceProvider as IlluminateServiceProvider;
13+
use Override;
14+
use Symfony\Component\Finder\Finder;
15+
16+
class ServiceProvider extends IlluminateServiceProvider implements DeferrableProvider
17+
{
18+
/** @return list<string> */
19+
#[Override]
20+
public function provides(): array
21+
{
22+
return [EntityManager::class];
23+
}
24+
25+
#[Override]
26+
public function register(): void
27+
{
28+
$this->app->singleton(EntityManager::class, function (Application $app) {
29+
return new EntityManager($this->getEntities($app), $app->make('db'));
30+
});
31+
}
32+
33+
/** @return Collection<int, Entity> */
34+
private function getEntities(Application $app): Collection
35+
{
36+
$composer = new Composer($app->make('files'), $app->basePath());
37+
38+
return collect()
39+
->wrap(iterator_to_array(
40+
Finder::create()
41+
->files()
42+
->in($app->basePath())
43+
->path('database/entities'),
44+
))
45+
->map(fn ($file) => (string) $file->getRealPath())
46+
->pipe(fn ($files) => collect($composer->classFromFile($files->all())))
47+
->filter(fn ($class) => is_subclass_of($class, Entity::class))
48+
->map(fn ($class) => $app->make($class))
49+
->values();
50+
}
51+
}

0 commit comments

Comments
 (0)