Skip to content

Commit 10a3145

Browse files
committed
updated model concepts
1 parent 8456fc5 commit 10a3145

1 file changed

Lines changed: 274 additions & 46 deletions

File tree

core-concepts.html

Lines changed: 274 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -848,90 +848,311 @@ <h4 class="text-lg font-semibold mb-3 text-gray-900">When to Use Each Approach</
848848
<section id="database-orm">
849849
<h2 class="text-2xl font-bold text-gray-900 mb-4">Database & ORM (Capabilities)</h2>
850850
<p class="text-gray-600 mb-4">
851-
Database and ORM are <strong>not built into the kernel</strong>. They're capabilities you install when you need them. Install a database capability like <code class="bg-gray-100 px-2 py-1 rounded">ForgeDatabaseSQL</code> or an ORM capability like <code class="bg-gray-100 px-2 py-1 rounded">ForgeSqlOrm</code>.
851+
Database and ORM are <strong>not built into the kernel</strong>. They're capabilities you install when you need them. The kernel provides <strong>contracts (interfaces)</strong> for database operations, but these contracts must be implemented by a module.
852852
</p>
853+
<p class="text-gray-600 mb-4">
854+
<strong>Important:</strong> The kernel provides <code class="bg-gray-100 px-2 py-1 rounded">DatabaseConnectionInterface</code> and <code class="bg-gray-100 px-2 py-1 rounded">QueryBuilderInterface</code> contracts, but they won't work unless you install a module that implements them. For example:
855+
</p>
856+
<ul class="list-disc list-inside space-y-2 text-gray-600 mb-4">
857+
<li><code class="bg-gray-100 px-2 py-1 rounded">ForgeDatabaseSQL</code> implements <code class="bg-gray-100 px-2 py-1 rounded">DatabaseConnectionInterface</code></li>
858+
<li><code class="bg-gray-100 px-2 py-1 rounded">ForgeSqlOrm</code> implements <code class="bg-gray-100 px-2 py-1 rounded">QueryBuilderInterface</code></li>
859+
<li>You can also create your own module that implements these contracts</li>
860+
</ul>
853861
<p class="text-gray-600 mb-4">
854862
Forge provides <strong>three ways to work with data</strong>:
855-
</p>
863+
</p>
856864
<ol class="list-decimal list-inside space-y-2 text-gray-600 mb-4">
857-
<li><strong>Raw SQL Queries:</strong> Use <code class="bg-gray-100 px-2 py-1 rounded">QueryBuilderInterface</code> or <code class="bg-gray-100 px-2 py-1 rounded">DatabaseConnectionInterface</code> directly, even without installing an ORM capability. This is available through the kernel's database contracts.</li>
858-
<li><strong>Query Builder:</strong> Use the fluent query builder interface for type-safe database operations.</li>
865+
<li><strong>Raw SQL Queries:</strong> Use <code class="bg-gray-100 px-2 py-1 rounded">DatabaseConnectionInterface</code> (requires ForgeDatabaseSQL) or <code class="bg-gray-100 px-2 py-1 rounded">QueryBuilderInterface</code> (requires ForgeSqlOrm) for direct database access.</li>
866+
<li><strong>Query Builder:</strong> Use the fluent query builder interface for type-safe database operations (requires ForgeSqlOrm).</li>
859867
<li><strong>ORM:</strong> Use the ForgeSqlOrm capability for attribute-based models and relationships.</li>
860868
</ol>
869+
870+
<h3 class="text-lg font-semibold mb-3 text-gray-900">Raw SQL Queries with DatabaseConnectionInterface</h3>
861871
<p class="text-gray-600 mb-4">
862-
<strong>Note:</strong> The ORM examples below assume you've installed the <code class="bg-gray-100 px-2 py-1 rounded">ForgeSqlOrm</code> capability. Raw queries and query builder are available through the kernel's database contracts.
872+
When using <code class="bg-gray-100 px-2 py-1 rounded">DatabaseConnectionInterface</code>, you have direct access to PDO methods. This requires the <code class="bg-gray-100 px-2 py-1 rounded">ForgeDatabaseSQL</code> module to be installed.
863873
</p>
874+
<div class="code-block p-6 text-white rounded-lg mb-6">
875+
<pre><code class="language-php">&lt;?php
876+
877+
use Forge\Core\Contracts\Database\DatabaseConnectionInterface;
878+
879+
class MyController
880+
{
881+
public function __construct(
882+
private readonly DatabaseConnectionInterface $connection
883+
) {}
884+
885+
// Execute raw SQL (DDL statements)
886+
public function createTable(): void
887+
{
888+
$this->connection->exec(
889+
"CREATE TABLE IF NOT EXISTS example_table (id INTEGER PRIMARY KEY, name TEXT)"
890+
);
891+
}
892+
893+
// Query with prepared statements
894+
public function findUser(int $id): array
895+
{
896+
$stmt = $this->connection->prepare("SELECT * FROM users WHERE id = :id");
897+
$stmt->execute([':id' => $id]);
898+
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
899+
}
864900

865-
<h3 class="text-lg font-semibold mb-3 text-gray-900">Raw SQL Queries</h3>
901+
// Simple query (no parameters)
902+
public function getAllUsers(): array
903+
{
904+
$stmt = $this->connection->query("SELECT * FROM users LIMIT 5");
905+
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
906+
}
907+
}</code></pre>
908+
</div>
909+
910+
<h3 class="text-lg font-semibold mb-3 text-gray-900">Raw SQL Queries with QueryBuilderInterface</h3>
866911
<p class="text-gray-600 mb-4">
867-
You can use raw SQL queries without installing any ORM capability. The kernel provides <code class="bg-gray-100 px-2 py-1 rounded">QueryBuilderInterface</code> and <code class="bg-gray-100 px-2 py-1 rounded">DatabaseConnectionInterface</code> for direct database access. This is how migrations work internally.
912+
When using <code class="bg-gray-100 px-2 py-1 rounded">QueryBuilderInterface</code>, you can use raw SQL methods or combine them with the query builder. This requires the <code class="bg-gray-100 px-2 py-1 rounded">ForgeSqlOrm</code> module to be installed.
868913
</p>
869914
<div class="code-block p-6 text-white rounded-lg mb-6">
870915
<pre><code class="language-php">&lt;?php
871916

872-
use Forge\Core\Contracts\Database\QueryBuilderInterface;
873-
use Forge\Core\Contracts\Database\DatabaseConnectionInterface;
874-
use Forge\Core\DI\Container;
917+
use App\Modules\ForgeSqlOrm\ORM\QueryBuilder;
875918

876-
// Using QueryBuilderInterface
877-
$queryBuilder = Container::getInstance()->get(QueryBuilderInterface::class);
878-
$results = $queryBuilder->table('users')
879-
->where('status', '=', 'active')
880-
->get();
919+
class MyController
920+
{
921+
public function __construct(
922+
private readonly QueryBuilder $builder
923+
) {}
881924

882-
// Using DatabaseConnectionInterface for raw SQL
883-
$connection = Container::getInstance()->get(DatabaseConnectionInterface::class);
884-
$stmt = $connection->prepare('SELECT * FROM users WHERE status = :status');
885-
$stmt->execute(['status' => 'active']);
886-
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);
925+
// Raw SQL query
926+
public function rawQuery(): array
927+
{
928+
return $this->builder->raw(
929+
"SELECT * FROM users WHERE status = :status",
930+
[':status' => 'active']
931+
);
932+
}
887933

888-
// Execute raw SQL
889-
$queryBuilder->execute('CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, title TEXT)');</code></pre>
934+
// WhereRaw with query builder
935+
public function whereRawExample(): array
936+
{
937+
return $this->builder
938+
->table('users')
939+
->whereRaw('status = :status', [':status' => 'active'])
940+
->get();
941+
}
942+
943+
// Combined: query builder + raw SQL
944+
public function combinedExample(): array
945+
{
946+
return $this->builder
947+
->table('users')
948+
->select('id', 'email', 'identifier')
949+
->where('status', '=', 'active')
950+
->whereRaw('identifier IS NOT NULL', [])
951+
->orderBy('created_at', 'DESC')
952+
->limit(10)
953+
->get();
954+
}
955+
}</code></pre>
956+
</div>
957+
958+
<h3 class="text-lg font-semibold mb-3 text-gray-900">Transactions</h3>
959+
<p class="text-gray-600 mb-4">
960+
Both interfaces support database transactions. Here are examples of commit and rollback operations:
961+
</p>
962+
963+
<h4 class="text-lg font-semibold mb-3 text-gray-900">Transactions with DatabaseConnectionInterface</h4>
964+
<div class="code-block p-6 text-white rounded-lg mb-6">
965+
<pre><code class="language-php">&lt;?php
966+
967+
// Transaction with commit
968+
public function transactionCommit(): array
969+
{
970+
$this->connection->beginTransaction();
971+
try {
972+
$stmt = $this->connection->prepare("INSERT INTO example_table (name) VALUES (:name)");
973+
$stmt->execute([':name' => 'transaction_test_commit']);
974+
$this->connection->commit();
975+
return ['status' => 'committed', 'message' => 'Transaction committed successfully'];
976+
} catch (\Exception $e) {
977+
$this->connection->rollBack();
978+
return ['status' => 'error', 'message' => $e->getMessage()];
979+
}
980+
}
981+
982+
// Transaction with rollback
983+
public function transactionRollback(): array
984+
{
985+
$this->connection->beginTransaction();
986+
try {
987+
$stmt = $this->connection->prepare("INSERT INTO example_table (name) VALUES (:name)");
988+
$stmt->execute([':name' => 'transaction_test_rollback']);
989+
throw new \Exception('Simulated error to trigger rollback');
990+
} catch (\Exception $e) {
991+
$this->connection->rollBack();
992+
return ['status' => 'rolled_back', 'message' => 'Transaction rolled back successfully'];
993+
}
994+
}</code></pre>
995+
</div>
996+
997+
<h4 class="text-lg font-semibold mb-3 text-gray-900">Transactions with QueryBuilderInterface</h4>
998+
<div class="code-block p-6 text-white rounded-lg mb-6">
999+
<pre><code class="language-php">&lt;?php
1000+
1001+
// Transaction with commit
1002+
public function transactionCommit(): array
1003+
{
1004+
try {
1005+
$this->builder->beginTransaction();
1006+
$id = $this->builder->table('example_table')->insert(['name' => 'orm_transaction_commit']);
1007+
$this->builder->commit();
1008+
return ['status' => 'committed', 'inserted_id' => $id];
1009+
} catch (\Exception $e) {
1010+
$this->builder->rollback();
1011+
return ['status' => 'error', 'message' => $e->getMessage()];
1012+
}
1013+
}
1014+
1015+
// Transaction with rollback
1016+
public function transactionRollback(): array
1017+
{
1018+
try {
1019+
$this->builder->beginTransaction();
1020+
$this->builder->table('example_table')->insert(['name' => 'orm_transaction_rollback']);
1021+
throw new \Exception('Simulated error to trigger rollback');
1022+
$this->builder->commit();
1023+
} catch (\Exception $e) {
1024+
$this->builder->rollback();
1025+
return ['status' => 'rolled_back', 'message' => 'Transaction rolled back successfully'];
1026+
}
1027+
}</code></pre>
8901028
</div>
8911029

8921030
<h3 class="text-lg font-semibold mb-3 text-gray-900">Model Definition</h3>
1031+
<p class="text-gray-600 mb-4">
1032+
Models extend the <code class="bg-gray-100 px-2 py-1 rounded">Model</code> base class and use attributes to define table structure, columns, and relationships. You can also use traits to add common functionality.
1033+
</p>
8931034
<div class="code-block p-6 text-white rounded-lg mb-6">
8941035
<pre><code class="language-php">&lt;?php
8951036

8961037
use App\Modules\ForgeSqlOrm\ORM\Model;
8971038
use App\Modules\ForgeSqlOrm\ORM\Attributes\Table;
8981039
use App\Modules\ForgeSqlOrm\ORM\Attributes\Column;
899-
use App\Modules\ForgeSqlOrm\ORM\Attributes\Hidden;
9001040
use App\Modules\ForgeSqlOrm\ORM\Attributes\ProtectedFields;
9011041
use App\Modules\ForgeSqlOrm\ORM\Values\Cast;
9021042
use App\Modules\ForgeSqlOrm\ORM\Values\Relate;
1043+
use App\Modules\ForgeSqlOrm\ORM\Values\Relation;
9031044
use App\Modules\ForgeSqlOrm\ORM\Values\RelationKind;
1045+
use App\Modules\ForgeSqlOrm\Traits\HasTimeStamps;
1046+
use App\Modules\ForgeSqlOrm\Traits\HasMetaData;
1047+
use App\Modules\ForgeSqlOrm\ORM\CanLoadRelations;
9041048

9051049
#[Table('users')]
906-
#[ProtectedFields('password', 'remember_token')]
1050+
#[ProtectedFields(['password'])]
9071051
class User extends Model
9081052
{
909-
#[Column(primary: true, cast: Cast::INTEGER)]
1053+
use HasTimeStamps; // Adds created_at and updated_at columns
1054+
use CanLoadRelations; // Enables relationship loading methods
1055+
use HasMetaData; // Adds metadata column for JSON data
1056+
1057+
#[Column(primary: true, cast: Cast::INT)]
9101058
public int $id;
911-
1059+
9121060
#[Column(cast: Cast::STRING)]
913-
public string $name;
914-
1061+
public string $status;
1062+
1063+
#[Column(cast: Cast::STRING)]
1064+
public string $identifier;
1065+
9151066
#[Column(cast: Cast::STRING)]
9161067
public string $email;
917-
1068+
9181069
#[Column(cast: Cast::STRING)]
919-
#[Hidden]
9201070
public string $password;
921-
1071+
1072+
#[Column(cast: Cast::JSON)]
1073+
public ?UserMetadataDto $metadata; // Uses HasMetaData trait
1074+
9221075
// Relationships use #[Relate] attribute
923-
#[Relate(
924-
kind: RelationKind::HasMany,
925-
target: Post::class,
926-
foreignKey: 'user_id',
927-
localKey: 'id'
928-
)]
929-
public function posts()
1076+
#[Relate(RelationKind::HasOne, Profile::class, "user_id")]
1077+
public function profile(): Relation
9301078
{
931-
return $this->relation('posts');
1079+
return self::describe(__FUNCTION__);
9321080
}
9331081
}</code></pre>
934-
</div>
1082+
</div>
1083+
1084+
<h4 class="text-lg font-semibold mb-3 text-gray-900">Available Traits</h4>
1085+
<p class="text-gray-600 mb-4">
1086+
ForgeSqlOrm provides several traits to add common functionality to your models:
1087+
</p>
1088+
1089+
<h5 class="text-lg font-semibold mb-3 text-gray-900">HasTimeStamps</h5>
1090+
<p class="text-gray-600 mb-4">
1091+
Automatically adds <code class="bg-gray-100 px-2 py-1 rounded">created_at</code> and <code class="bg-gray-100 px-2 py-1 rounded">updated_at</code> timestamp columns to your model. These are automatically managed by the ORM when creating or updating records.
1092+
</p>
1093+
<div class="code-block p-6 text-white rounded-lg mb-6">
1094+
<pre><code class="language-php">&lt;?php
1095+
1096+
use App\Modules\ForgeSqlOrm\Traits\HasTimeStamps;
1097+
1098+
class User extends Model
1099+
{
1100+
use HasTimeStamps;
1101+
1102+
// Automatically adds:
1103+
// public ?DateTimeImmutable $created_at = null;
1104+
// public ?DateTimeImmutable $updated_at = null;
1105+
}</code></pre>
1106+
</div>
1107+
1108+
<h5 class="text-lg font-semibold mb-3 text-gray-900">HasMetaData</h5>
1109+
<p class="text-gray-600 mb-4">
1110+
Adds a <code class="bg-gray-100 px-2 py-1 rounded">metadata</code> column for storing JSON data. Useful for flexible, schema-less data that doesn't need its own table.
1111+
</p>
1112+
<div class="code-block p-6 text-white rounded-lg mb-6">
1113+
<pre><code class="language-php">&lt;?php
1114+
1115+
use App\Modules\ForgeSqlOrm\Traits\HasMetaData;
1116+
1117+
class User extends Model
1118+
{
1119+
use HasMetaData;
1120+
1121+
// Automatically adds:
1122+
// public ?array $metadata = null;
1123+
1124+
// Usage:
1125+
// $user->metadata = ['preferences' => ['theme' => 'dark']];
1126+
// $user->save();
1127+
}</code></pre>
1128+
</div>
1129+
1130+
<h5 class="text-lg font-semibold mb-3 text-gray-900">CanLoadRelations</h5>
1131+
<p class="text-gray-600 mb-4">
1132+
Provides methods for loading and working with relationships. This trait is already included in the <code class="bg-gray-100 px-2 py-1 rounded">Model</code> base class, but you can use it explicitly if needed.
1133+
</p>
1134+
<p class="text-gray-600 mb-4">
1135+
Key methods:
1136+
</p>
1137+
<ul class="list-disc list-inside space-y-2 text-gray-600 mb-4">
1138+
<li><code class="bg-gray-100 px-2 py-1 rounded">with(string ...$paths)</code> — Eager load relationships when querying</li>
1139+
<li><code class="bg-gray-100 px-2 py-1 rounded">load(string ...$relations)</code> — Lazy load relationships on an existing model instance</li>
1140+
<li><code class="bg-gray-100 px-2 py-1 rounded">relation(string $name)</code> — Get a query builder for a relationship</li>
1141+
<li><code class="bg-gray-100 px-2 py-1 rounded">describe(string $method)</code> — Get relationship metadata</li>
1142+
</ul>
1143+
<div class="code-block p-6 text-white rounded-lg mb-6">
1144+
<pre><code class="language-php">&lt;?php
1145+
1146+
// Eager loading relationships
1147+
$user = User::with('profile')->id(1)->first();
1148+
1149+
// Lazy loading relationships
1150+
$user = User::query()->id(1)->first();
1151+
$user->load('profile');
1152+
1153+
// Using relation() to build queries
1154+
$posts = $user->relation('posts')->where('status', '=', 'published')->get();</code></pre>
1155+
</div>
9351156

9361157
<h3 class="text-lg font-semibold mb-3 text-gray-900">Query Builder</h3>
9371158
<div class="code-block p-6 text-white rounded-lg mb-6">
@@ -953,18 +1174,25 @@ <h3 class="text-lg font-semibold mb-3 text-gray-900">Query Builder</h3>
9531174
->where('created_at', '>', '2024-01-01')
9541175
->get();
9551176

956-
// With relationships
1177+
// Eager load relationships (using with() static method)
1178+
$user = User::with('profile', 'posts')->id(1)->first();
1179+
1180+
// Or using query builder with with()
9571181
$user = User::query()
958-
->with('posts')
1182+
->with('profile', 'posts')
9591183
->id(1)
9601184
->first();
9611185

962-
// Access relationship
963-
$posts = $user->load('posts');</code></pre>
1186+
// Lazy load relationships on existing instance
1187+
$user = User::query()->id(1)->first();
1188+
$user->load('profile', 'posts');
1189+
1190+
// Access relationship using relation() method
1191+
$posts = $user->relation('posts')->where('status', '=', 'published')->get();</code></pre>
9641192
</div>
9651193
<p class="text-gray-600 mb-4">
966-
<strong>Note:</strong> Forge uses a query builder pattern. Always start with <code>Model::query()</code> to build queries. There are no static methods like <code>all()</code> or <code>find()</code> directly on the Model class.
967-
</p>
1194+
<strong>Note:</strong> Forge uses a query builder pattern. Always start with <code class="bg-gray-100 px-2 py-1 rounded">Model::query()</code> to build queries. There are no static methods like <code class="bg-gray-100 px-2 py-1 rounded">all()</code> or <code class="bg-gray-100 px-2 py-1 rounded">find()</code> directly on the Model class. Use <code class="bg-gray-100 px-2 py-1 rounded">with()</code> for eager loading relationships, or <code class="bg-gray-100 px-2 py-1 rounded">load()</code> for lazy loading on existing instances.
1195+
</p>
9681196
</section>
9691197

9701198
<section id="capability-system">

0 commit comments

Comments
 (0)