Skip to content

Commit 2b599c6

Browse files
committed
docs: enhance PHPDoc with examples, warnings, and documentation links
Add comprehensive @example tags with usage examples for: - DmlQueryBuilder::insertFrom() - INSERT ... SELECT operations - SelectQueryBuilder::getColumn() - column extraction with index() - DmlQueryBuilder::merge() - MERGE/UPSERT operations - SelectQueryBuilder::paginate() - pagination with metadata - QueryBuilder::withRecursive() - recursive CTEs for hierarchies - ColumnSchema::defaultValue() - default value handling - SelectQueryBuilder::groupBy() - GROUP BY with JOINs - DdlBuilderTrait::createIndex() - advanced index creation - Model::relations() - ActiveRecord relationships - ConditionalHelpersTrait::case() - CASE statements Add @warning tags for: - MSSQL IDENTITY column handling in insertFrom() - Dialect-specific differences in merge() and createIndex() - Performance considerations in paginate() - MSSQL reserved words in case() and groupBy() Add @see tags linking to relevant documentation files. Improve developer experience with inline examples and clear warnings.
1 parent b5e010c commit 2b599c6

File tree

7 files changed

+376
-2
lines changed

7 files changed

+376
-2
lines changed

src/helpers/traits/ConditionalHelpersTrait.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,51 @@ trait ConditionalHelpersTrait
1414
/**
1515
* Returns a RawValue instance representing a CASE statement.
1616
*
17+
* Generates a SQL CASE expression with automatic identifier quoting for MSSQL reserved words.
18+
*
1719
* @param array<string, string> $cases An associative array where keys are WHEN conditions and values are THEN results.
1820
* @param string|null $else An optional ELSE result.
1921
*
2022
* @return RawValue The RawValue instance for the CASE statement.
23+
*
24+
* @example
25+
* // Simple CASE statement
26+
* $db->find()
27+
* ->from('users')
28+
* ->select([
29+
* 'status_label' => Db::case([
30+
* "status = 'active'" => "'Active'",
31+
* "status = 'inactive'" => "'Inactive'",
32+
* ], "'Unknown'")
33+
* ])
34+
* ->get();
35+
* @example
36+
* // CASE with MSSQL reserved words (automatically quoted)
37+
* $db->find()
38+
* ->from('plans')
39+
* ->select([
40+
* 'plan_type' => Db::case([
41+
* "plan = 'basic'" => "'Basic Plan'",
42+
* "plan = 'premium'" => "'Premium Plan'",
43+
* ])
44+
* ])
45+
* ->get();
46+
* @example
47+
* // CASE in WHERE clause
48+
* $db->find()
49+
* ->from('orders')
50+
* ->where(Db::case([
51+
* "status = 'pending'" => '1',
52+
* "status = 'processing'" => '1',
53+
* ], '0'), '1')
54+
* ->get();
55+
*
56+
* @note For MSSQL: Identifiers in WHEN conditions are automatically quoted if they are
57+
* reserved words (e.g., 'plan', 'order', 'group'). This prevents syntax errors.
58+
*
59+
* @warning WHEN conditions should be valid SQL expressions. Use Db::raw() for complex expressions.
60+
*
61+
* @see documentation/07-helper-functions/06-comparison-helpers.md
2162
*/
2263
public static function case(array $cases, string|null $else = null): RawValue
2364
{

src/orm/Model.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,81 @@ public static function rules(): array
9696
/**
9797
* Get relationship definitions.
9898
*
99+
* Defines ActiveRecord relationships for lazy and eager loading.
100+
* Supports hasOne, hasMany, belongsTo, and hasManyThrough relationships.
101+
*
99102
* Format: [
100103
* 'relationName' => ['hasOne', 'modelClass' => RelatedModel::class, 'foreignKey' => '...', 'localKey' => '...'],
101104
* 'relationName' => ['hasMany', 'modelClass' => RelatedModel::class, 'foreignKey' => '...', 'localKey' => '...'],
102105
* 'relationName' => ['belongsTo', 'modelClass' => RelatedModel::class, 'foreignKey' => '...', 'ownerKey' => '...'],
106+
* 'relationName' => ['hasManyThrough', 'modelClass' => RelatedModel::class, 'viaTable' => '...', 'link' => [...], 'viaLink' => [...]],
103107
* ]
104108
*
105109
* Override this method to define relationships for the model.
106110
*
107111
* @return array<string, array<int|string, mixed>> Relationship definitions
112+
*
113+
* @example
114+
* // User has one Profile
115+
* public static function relations(): array
116+
* {
117+
* return [
118+
* 'profile' => [
119+
* 'hasOne',
120+
* 'modelClass' => Profile::class,
121+
* 'foreignKey' => 'user_id',
122+
* 'localKey' => 'id'
123+
* ]
124+
* ];
125+
* }
126+
* @example
127+
* // User has many Posts
128+
* public static function relations(): array
129+
* {
130+
* return [
131+
* 'posts' => [
132+
* 'hasMany',
133+
* 'modelClass' => Post::class,
134+
* 'foreignKey' => 'user_id',
135+
* 'localKey' => 'id'
136+
* ]
137+
* ];
138+
* }
139+
* @example
140+
* // Post belongs to User
141+
* public static function relations(): array
142+
* {
143+
* return [
144+
* 'user' => [
145+
* 'belongsTo',
146+
* 'modelClass' => User::class,
147+
* 'foreignKey' => 'user_id',
148+
* 'ownerKey' => 'id'
149+
* ]
150+
* ];
151+
* }
152+
* @example
153+
* // Many-to-many via junction table
154+
* public static function relations(): array
155+
* {
156+
* return [
157+
* 'projects' => [
158+
* 'hasManyThrough',
159+
* 'modelClass' => Project::class,
160+
* 'viaTable' => 'user_project',
161+
* 'link' => ['id' => 'user_id'],
162+
* 'viaLink' => ['project_id' => 'id']
163+
* ]
164+
* ];
165+
* }
166+
*
167+
* @note Relationships can be accessed as properties (lazy loading) or via with() (eager loading).
168+
* Use eager loading to prevent N+1 query problems.
169+
*
170+
* @warning Foreign key and local key parameters are required for all relationship types.
171+
*
172+
* @see Model::with() For eager loading relationships
173+
* @see documentation/05-advanced-features/17-active-record-relationships.md
108174
*/
109175
public static function relations(): array
110176
{

src/query/DmlQueryBuilder.php

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,42 @@ public function insert(array $data, array $onDuplicate = []): int
197197
/**
198198
* Insert data from a SELECT query or subquery.
199199
*
200+
* Supports INSERT ... SELECT with ON DUPLICATE KEY UPDATE (MySQL/MariaDB),
201+
* ON CONFLICT (PostgreSQL), and MERGE (MSSQL).
202+
*
200203
* @param string|\Closure(QueryBuilder): void|SelectQueryBuilderInterface|QueryBuilder $source Source query (table name, QueryBuilder, SelectQueryBuilderInterface, or Closure)
201204
* @param array<string>|null $columns Column names to insert into (null = use SELECT columns)
202205
* @param array<string, string|int|float|bool|null|RawValue> $onDuplicate The columns to update on duplicate.
203206
*
204207
* @return int The result of the insert operation.
208+
*
209+
* @example
210+
* // Basic INSERT ... SELECT
211+
* $db->find()
212+
* ->table('target_table')
213+
* ->insertFrom('source_table');
214+
* @example
215+
* // INSERT ... SELECT with specific columns
216+
* $db->find()
217+
* ->table('target_table')
218+
* ->insertFrom('source_table', ['id', 'name', 'email']);
219+
* @example
220+
* // INSERT ... SELECT with ON DUPLICATE KEY UPDATE (MySQL)
221+
* $db->find()
222+
* ->table('target_table')
223+
* ->insertFrom('source_table', null, ['name' => Db::raw('VALUES(name)'), 'updated_at' => Db::now()]);
224+
* @example
225+
* // INSERT ... SELECT from QueryBuilder
226+
* $db->find()
227+
* ->table('target_table')
228+
* ->insertFrom(function($query) {
229+
* $query->from('source_table')->where('status', 'active');
230+
* });
231+
*
232+
* @warning For MSSQL: IDENTITY columns are automatically excluded from INSERT column list
233+
* when columns are not explicitly provided. This prevents "IDENTITY_INSERT" errors.
234+
*
235+
* @see documentation/03-query-builder/02-data-manipulation.md#insert--select-copy-data-between-tables
205236
*/
206237
public function insertFrom(
207238
string|\Closure|SelectQueryBuilderInterface|QueryBuilder $source,
@@ -869,14 +900,49 @@ protected function processValueForSql(mixed $value, string $columnName, string $
869900
/**
870901
* Execute MERGE statement (INSERT/UPDATE/DELETE based on match conditions).
871902
*
903+
* Performs INSERT ... ON CONFLICT UPDATE (PostgreSQL), INSERT ... ON DUPLICATE KEY UPDATE (MySQL),
904+
* or MERGE (MSSQL) operations. This is a powerful way to synchronize data between tables.
905+
*
872906
* @param string|\Closure(QueryBuilder): void|SelectQueryBuilderInterface $source Source table/subquery for MERGE
873-
* @param string|array<string> $onConditions ON clause conditions
907+
* @param string|array<string> $onConditions ON clause conditions (e.g., ['target.id = source.id'])
874908
* @param array<string, string|int|float|bool|null|RawValue> $whenMatched Update columns when matched
875909
* @param array<string, string|int|float|bool|null|RawValue> $whenNotMatched Insert columns when not matched
876910
* @param bool $whenNotMatchedBySourceDelete Delete when not matched by source
877911
*
878912
* @return int Number of affected rows
879913
* @throws RuntimeException If MERGE is not supported by dialect
914+
*
915+
* @example
916+
* // MERGE/UPSERT from table
917+
* $db->find()
918+
* ->table('target_users')
919+
* ->merge('source_users', ['target_users.id = source_users.id'], [
920+
* 'name' => Db::raw('source_users.name'),
921+
* 'updated_at' => Db::now()
922+
* ], [
923+
* 'id' => Db::raw('source_users.id'),
924+
* 'name' => Db::raw('source_users.name')
925+
* ]);
926+
* @example
927+
* // MERGE/UPSERT from QueryBuilder
928+
* $db->find()
929+
* ->table('target_users')
930+
* ->merge(function($query) {
931+
* $query->from('source_users')->where('status', 'active');
932+
* }, ['target_users.id = source_users.id'], [
933+
* 'name' => Db::raw('source_users.name')
934+
* ]);
935+
*
936+
* @warning MERGE syntax differs between databases:
937+
* - PostgreSQL: Uses INSERT ... ON CONFLICT
938+
* - MySQL/MariaDB: Uses INSERT ... ON DUPLICATE KEY UPDATE
939+
* - MSSQL: Uses MERGE statement
940+
* - SQLite: Not supported, throws exception
941+
*
942+
* @note For MSSQL, use table aliases in ON conditions (e.g., 'target.id = source.id').
943+
* For PostgreSQL/MySQL, the conflict target is automatically detected from ON conditions.
944+
*
945+
* @see documentation/05-advanced-features/05-upsert-operations.md
880946
*/
881947
public function merge(
882948
string|\Closure|SelectQueryBuilderInterface $source,

src/query/QueryBuilder.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,11 +455,53 @@ public function with(
455455
/**
456456
* Add a recursive Common Table Expression (CTE) to the query.
457457
*
458+
* Creates a recursive Common Table Expression (WITH RECURSIVE) for hierarchical queries,
459+
* tree traversal, and other recursive data structures.
460+
*
458461
* @param string $name CTE name
459462
* @param QueryBuilder|Closure(QueryBuilder): void|string|RawValue $query Query with UNION ALL structure
460463
* @param array<string> $columns Explicit column list (recommended for recursive CTEs)
461464
*
462465
* @return static The current instance.
466+
*
467+
* @example
468+
* // Hierarchical category tree
469+
* $categories = $db->find()
470+
* ->withRecursive('category_tree', function($q) {
471+
* $q->from('categories')
472+
* ->select(['id', 'name', 'parent_id', Db::raw('0 as level')])
473+
* ->where('parent_id', null, 'IS');
474+
* }, function($r) {
475+
* $r->from('categories c')
476+
* ->select(['c.id', 'c.name', 'c.parent_id', Db::raw('ct.level + 1 as level')])
477+
* ->join('category_tree ct', 'c.parent_id = ct.id');
478+
* })
479+
* ->from('category_tree')
480+
* ->orderBy('level')
481+
* ->orderBy('name')
482+
* ->get();
483+
* @example
484+
* // Employee hierarchy
485+
* $employees = $db->find()
486+
* ->withRecursive('employee_tree', function($q) {
487+
* $q->from('employees')
488+
* ->select(['id', 'name', 'manager_id', Db::raw('1 as depth')])
489+
* ->where('manager_id', null, 'IS');
490+
* }, function($r) {
491+
* $r->from('employees e')
492+
* ->select(['e.id', 'e.name', 'e.manager_id', Db::raw('et.depth + 1')])
493+
* ->join('employee_tree et', 'e.manager_id = et.id');
494+
* })
495+
* ->from('employee_tree')
496+
* ->get();
497+
*
498+
* @warning Recursive CTEs require careful design to avoid infinite loops.
499+
* Always ensure the recursive part eventually terminates.
500+
*
501+
* @note Column names are auto-detected from SELECT clauses if not provided.
502+
* Explicit column names improve performance and prevent errors.
503+
*
504+
* @see documentation/03-query-builder/08-cte.md#recursive-ctes
463505
*/
464506
public function withRecursive(
465507
string $name,

0 commit comments

Comments
 (0)