77use RuntimeException ;
88use SimpleXMLElement ;
99use Yii ;
10+ use yii \base \InvalidArgumentException ;
1011use yii \console \Application ;
11- use yii \db \{Connection , SchemaBuilderTrait };
12+ use yii \db \{ActiveQuery , ActiveRecord , Connection , SchemaBuilderTrait };
1213use yii2 \extensions \nestedsets \tests \support \model \{MultipleTree , Tree };
1314
1415use function array_merge ;
3031 * depth: int,
3132 * }
3233 * >
34+ * @phpstan-type NodeChildren array<string|array{name: string, children?: array<mixed>}>
35+ * @phpstan-type TreeStructure array<array<mixed>>
36+ * @phpstan-type UpdateData array<array{name: string, lft?: int, rgt?: int, depth?: int}>
3337 */
3438class TestCase extends \PHPUnit \Framework \TestCase
3539{
3640 use SchemaBuilderTrait;
3741
42+ protected string |null $ dsn = null ;
3843 protected string $ fixtureDirectory = __DIR__ . '/support/data/ ' ;
3944
4045 protected function setUp (): void
@@ -49,6 +54,66 @@ public function getDb(): Connection
4954 return Yii::$ app ->getDb ();
5055 }
5156
57+ /**
58+ * Asserts that a list of tree nodes matches the expected order.
59+ *
60+ * @param array $nodesList List of tree nodes to validate
61+ * @param array $expectedOrder Expected order of node names
62+ * @param string $nodeType Type of nodes being tested (for error messages)
63+ *
64+ * @phpstan-param array<ActiveRecord> $nodesList
65+ * @phpstan-param array<string> $expectedOrder
66+ */
67+ protected function assertNodesInCorrectOrder (array $ nodesList , array $ expectedOrder , string $ nodeType ): void
68+ {
69+ self ::assertCount (
70+ count ($ expectedOrder ),
71+ $ nodesList ,
72+ "{$ nodeType } list should contain exactly ' " . count ($ expectedOrder ) . "' elements. " ,
73+ );
74+
75+ foreach ($ nodesList as $ index => $ node ) {
76+ self ::assertInstanceOf (
77+ Tree::class,
78+ $ node ,
79+ "{$ nodeType } at index {$ index } should be an instance of 'Tree'. " ,
80+ );
81+
82+ if (isset ($ expectedOrder [$ index ])) {
83+ self ::assertEquals (
84+ $ expectedOrder [$ index ],
85+ $ node ->getAttribute ('name ' ),
86+ "{$ nodeType } at index {$ index } should be {$ expectedOrder [$ index ]} in correct 'lft' order. " ,
87+ );
88+ }
89+ }
90+ }
91+
92+ /**
93+ * Asserts that a query contains ORDER BY clause with 'lft' column.
94+ *
95+ * @param ActiveQuery $query The query to check
96+ * @param string $methodName Name of the method being tested
97+ *
98+ * @phpstan-param ActiveQuery<ActiveRecord> $query
99+ */
100+ protected function assertQueryHasOrderBy (ActiveQuery $ query , string $ methodName ): void
101+ {
102+ $ sql = $ query ->createCommand ()->getRawSql ();
103+
104+ self ::assertStringContainsString (
105+ 'ORDER BY ' ,
106+ $ sql ,
107+ "' {$ methodName }' query should include 'ORDER BY' clause for deterministic results. " ,
108+ );
109+
110+ self ::assertStringContainsString (
111+ '`lft` ' ,
112+ $ sql ,
113+ "' {$ methodName }' query should order by 'left' attribute for consistent ordering. " ,
114+ );
115+ }
116+
52117 /**
53118 * @phpstan-import-type DataSetType from TestCase
54119 *
@@ -133,6 +198,56 @@ protected function createDatabase(): void
133198 )->execute ();
134199 }
135200
201+ /**
202+ * Creates a tree structure based on a hierarchical definition.
203+ *
204+ * @param array $structure Hierarchical tree structure definition
205+ * @param array $updates Database updates to apply after creation
206+ * @param string $modelClass The model class to use (Tree::class or MultipleTree::class)
207+ *
208+ * @throws InvalidArgumentException if the structure array is empty.
209+ *
210+ * @return MultipleTree|Tree The root node
211+ *
212+ * @phpstan-param TreeStructure $structure
213+ * @phpstan-param UpdateData $updates
214+ * @phpstan-param class-string<Tree|MultipleTree> $modelClass
215+ */
216+ protected function createTreeStructure (
217+ array $ structure ,
218+ array $ updates = [],
219+ string $ modelClass = Tree::class,
220+ ): Tree |MultipleTree {
221+ if ($ structure === []) {
222+ throw new InvalidArgumentException ('Tree structure cannot be empty. ' );
223+ }
224+
225+ $ this ->createDatabase ();
226+
227+ $ rootNode = null ;
228+
229+ foreach ($ structure as $ rootDefinition ) {
230+ $ root = new $ modelClass (['name ' => $ rootDefinition ['name ' ] ?? 'Root ' ]);
231+ $ root ->makeRoot ();
232+
233+ if ($ rootNode === null ) {
234+ $ rootNode = $ root ;
235+ }
236+
237+ if (isset ($ rootDefinition ['children ' ])) {
238+ /** @phpstan-var NodeChildren $children */
239+ $ children = $ rootDefinition ['children ' ];
240+ $ this ->createChildrenRecursively ($ root , $ children );
241+ }
242+ }
243+
244+ $ this ->applyUpdates ($ updates , $ modelClass === MultipleTree::class ? 'multiple_tree ' : 'tree ' );
245+
246+ $ rootNode ->refresh ();
247+
248+ return $ rootNode ;
249+ }
250+
136251 protected function generateFixtureTree (): void
137252 {
138253 $ this ->createDatabase ();
@@ -236,10 +351,62 @@ protected function mockConsoleApplication(): void
236351 'components ' => [
237352 'db ' => [
238353 'class ' => Connection::class,
239- 'dsn ' => 'sqlite::memory: ' ,
354+ 'dsn ' => $ this -> dsn !== null ? $ this -> dsn : 'sqlite::memory: ' ,
240355 ],
241356 ],
242357 ],
243358 );
244359 }
360+
361+ /**
362+ * Applies database updates to tree nodes.
363+ *
364+ * @param array $updates Array of updates to apply.
365+ * @param string $tableName Name of the table to apply updates to.
366+ *
367+ * @phpstan-param UpdateData $updates
368+ */
369+ private function applyUpdates (array $ updates , string $ tableName ): void
370+ {
371+ if ($ updates === []) {
372+ return ;
373+ }
374+
375+ $ command = $ this ->getDb ()->createCommand ();
376+
377+ foreach ($ updates as $ update ) {
378+ $ name = $ update ['name ' ];
379+
380+ unset($ update ['name ' ]);
381+
382+ $ command ->update ($ tableName , $ update , ['name ' => $ name ])->execute ();
383+ }
384+ }
385+
386+ /**
387+ * Recursively creates children for a given parent node.
388+ *
389+ * @param MultipleTree|Tree $parent The parent node
390+ * @param array $nodes Children definition (can be strings or arrays)
391+ *
392+ * @phpstan-param NodeChildren $nodes
393+ */
394+ private function createChildrenRecursively (Tree |MultipleTree $ parent , array $ nodes ): void
395+ {
396+ foreach ($ nodes as $ nodeDefinition ) {
397+ if (is_string ($ nodeDefinition )) {
398+ $ node = new ($ parent ::class)(['name ' => $ nodeDefinition ]);
399+ $ node ->appendTo ($ parent );
400+ } else {
401+ $ node = new ($ parent ::class)(['name ' => $ nodeDefinition ['name ' ]]);
402+ $ node ->appendTo ($ parent );
403+
404+ if (isset ($ nodeDefinition ['children ' ])) {
405+ /** @phpstan-var NodeChildren $children */
406+ $ children = $ nodeDefinition ['children ' ];
407+ $ this ->createChildrenRecursively ($ node , $ children );
408+ }
409+ }
410+ }
411+ }
245412}
0 commit comments