• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saygoweb / anorm / 18485531299

14 Oct 2025 04:31AM UTC coverage: 84.381% (+3.9%) from 80.483%
18485531299

Pull #42

github

web-flow
Merge 93e4d6a17 into 025025bcf
Pull Request #42: Implement comprehensive join optimization system to solve N+1 query problem

847 of 962 new or added lines in 19 files covered. (88.05%)

18 existing lines in 1 file now uncovered.

1637 of 1940 relevant lines covered (84.38%)

15.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.51
/src/Relationship/Strategy/NestedRelationshipParser.php
1
<?php
2

3
namespace Anorm\Relationship\Strategy;
4

5
/**
6
 * Parser for nested relationship specifications
7
 *
8
 * Handles syntax like:
9
 * - 'posts.comments'
10
 * - 'posts.comments.author'
11
 * - 'company.users.posts'
12
 */
13
class NestedRelationshipParser
14
{
15
    /** @var array Circular reference detection */
16
    private $loadingStack = [];
17

18
    /**
19
     * Parse nested relationship specifications
20
     *
21
     * @param array $relationshipSpecs Array of relationship specifications
22
     * @return array Parsed nested structure
23
     */
24
    public function parseNestedSpecs(array $relationshipSpecs): array
25
    {
26
        $parsed = [];
4✔
27

28
        foreach ($relationshipSpecs as $spec) {
4✔
29
            $this->parseNestedSpec($spec, $parsed);
4✔
30
        }
31

32
        return $parsed;
4✔
33
    }
34

35
    /**
36
     * Parse a single nested relationship specification
37
     *
38
     * @param string $spec Relationship specification (e.g., 'posts.comments.author')
39
     * @param array &$parsed Reference to parsed structure
40
     * @return void
41
     */
42
    private function parseNestedSpec(string $spec, array &$parsed): void
43
    {
44
        // Handle field selection syntax: 'posts:id,title.comments:id,content'
45
        $parts = explode('.', $spec);
4✔
46
        $current = &$parsed;
4✔
47

48
        foreach ($parts as $part) {
4✔
49
            // Parse field selection for this level
50
            $fieldParser = new FieldSelectionParser();
4✔
51
            $parsedPart = $fieldParser->parseFieldSelection($part);
4✔
52

53
            $relationshipName = $parsedPart['relationship'];
4✔
54
            $fields = $parsedPart['fields'];
4✔
55

56
            if (!isset($current[$relationshipName])) {
4✔
57
                $current[$relationshipName] = [
4✔
58
                    'fields' => $fields,
4✔
59
                    'nested' => []
4✔
60
                ];
4✔
61
            } else {
62
                // Merge field selections if both exist
63
                if ($fields !== null && $current[$relationshipName]['fields'] !== null) {
1✔
NEW
64
                    $current[$relationshipName]['fields'] = array_unique(
×
NEW
65
                        array_merge($current[$relationshipName]['fields'], $fields)
×
NEW
66
                    );
×
67
                } elseif ($fields !== null) {
1✔
NEW
68
                    $current[$relationshipName]['fields'] = $fields;
×
69
                }
70
            }
71

72
            $current = &$current[$relationshipName]['nested'];
4✔
73
        }
74
    }
75

76
    /**
77
     * Load nested relationships for a set of models
78
     *
79
     * @param array $models Array of model instances
80
     * @param array $nestedSpecs Parsed nested relationship specifications
81
     * @param int $maxDepth Maximum nesting depth to prevent infinite recursion
82
     * @return void
83
     */
84
    public function loadNestedRelationships(array $models, array $nestedSpecs, int $maxDepth = 5): void
85
    {
86
        if (empty($models) || empty($nestedSpecs) || $maxDepth <= 0) {
4✔
87
            return;
1✔
88
        }
89

90
        foreach ($nestedSpecs as $relationshipName => $spec) {
4✔
91
            $this->loadSingleNestedRelationship($models, $relationshipName, $spec, $maxDepth);
4✔
92
        }
93
    }
94

95
    /**
96
     * Load a single nested relationship
97
     *
98
     * @param array $models Source models
99
     * @param string $relationshipName Name of the relationship to load
100
     * @param array $spec Relationship specification
101
     * @param int $maxDepth Remaining depth
102
     * @return void
103
     */
104
    private function loadSingleNestedRelationship(array $models, string $relationshipName, array $spec, int $maxDepth): void
105
    {
106
        // Detect circular references
107
        $circularKey = $this->generateCircularKey($models, $relationshipName);
4✔
108
        if (in_array($circularKey, $this->loadingStack, true)) {
4✔
NEW
109
            return; // Skip circular reference
×
110
        }
111

112
        $this->loadingStack[] = $circularKey;
4✔
113

114
        try {
115
            // Load the immediate relationship
116
            $this->loadImmediateRelationship($models, $relationshipName, $spec['fields']);
4✔
117

118
            // If there are nested relationships, load them recursively
119
            if (!empty($spec['nested'])) {
4✔
120
                $relatedModels = $this->extractRelatedModels($models, $relationshipName);
2✔
121
                $this->loadNestedRelationships($relatedModels, $spec['nested'], $maxDepth - 1);
2✔
122
            }
123
        } finally {
124
            // Remove from loading stack
125
            array_pop($this->loadingStack);
4✔
126
        }
127
    }
128

129
    /**
130
     * Load immediate relationship for models
131
     *
132
     * @param array $models Source models
133
     * @param string $relationshipName Relationship to load
134
     * @param array|null $fields Field selection
135
     * @return void
136
     */
137
    private function loadImmediateRelationship(array $models, string $relationshipName, ?array $fields): void
138
    {
139
        if (empty($models)) {
4✔
NEW
140
            return;
×
141
        }
142

143
        $firstModel = reset($models);
4✔
144
        $relationshipManager = $firstModel->getRelationshipManager();
4✔
145
        $relationship = $relationshipManager->getRelationship($relationshipName);
4✔
146

147
        if (!$relationship) {
4✔
NEW
148
            return; // Relationship not found
×
149
        }
150

151
        // Load using batch loading with field selection
152
        $batchResults = $relationship->batchLoad($models, $firstModel->getPdo(), $fields);
4✔
153
        $relationship->distributeBatchResults($models, $batchResults, $relationshipName);
4✔
154
    }
155

156
    /**
157
     * Extract related models from loaded relationships
158
     *
159
     * @param array $models Source models
160
     * @param string $relationshipName Relationship name
161
     * @return array Related models
162
     */
163
    private function extractRelatedModels(array $models, string $relationshipName): array
164
    {
165
        $relatedModels = [];
4✔
166

167
        foreach ($models as $model) {
4✔
168
            if (isset($model->{$relationshipName})) {
4✔
169
                $related = $model->{$relationshipName};
4✔
170

171
                if (is_array($related)) {
4✔
172
                    // One-to-many or many-to-many relationship
173
                    $relatedModels = array_merge($relatedModels, $related);
3✔
174
                } elseif ($related !== null) {
1✔
175
                    // Many-to-one or one-to-one relationship
176
                    $relatedModels[] = $related;
1✔
177
                }
178
            }
179
        }
180

181
        return $relatedModels;
4✔
182
    }
183

184
    /**
185
     * Generate a key for circular reference detection
186
     *
187
     * @param array $models Source models
188
     * @param string $relationshipName Relationship name
189
     * @return string Circular reference key
190
     */
191
    private function generateCircularKey(array $models, string $relationshipName): string
192
    {
193
        if (empty($models)) {
4✔
NEW
194
            return $relationshipName;
×
195
        }
196

197
        $firstModel = reset($models);
4✔
198
        $modelClass = get_class($firstModel);
4✔
199

200
        return $modelClass . '.' . $relationshipName;
4✔
201
    }
202

203
    /**
204
     * Validate nested relationship specification
205
     *
206
     * @param string $spec Relationship specification
207
     * @return array Validation result
208
     */
209
    public function validateNestedSpec(string $spec): array
210
    {
211
        $errors = [];
26✔
212
        $warnings = [];
26✔
213

214
        // Check for excessive nesting depth
215
        $depth = substr_count($spec, '.') + 1;
26✔
216
        if ($depth > 5) {
26✔
217
            $warnings[] = "Deep nesting detected ({$depth} levels). Consider optimizing for performance.";
1✔
218
        }
219

220
        // Check for potential circular references in spec
221
        $parts = explode('.', $spec);
26✔
222
        $seen = [];
26✔
223
        foreach ($parts as $part) {
26✔
224
            $relationshipName = explode(':', $part)[0]; // Remove field selection
26✔
225
            if (in_array($relationshipName, $seen, true)) {
26✔
226
                $errors[] = "Potential circular reference detected: {$relationshipName} appears multiple times.";
1✔
227
            }
228
            $seen[] = $relationshipName;
26✔
229
        }
230

231
        // Validate field selection syntax
232
        $fieldParser = new FieldSelectionParser();
26✔
233
        foreach ($parts as $part) {
26✔
234
            try {
235
                $fieldParser->parseFieldSelection($part);
26✔
NEW
236
            } catch (\Exception $e) {
×
NEW
237
                $errors[] = "Invalid field selection syntax in '{$part}': " . $e->getMessage();
×
238
            }
239
        }
240

241
        return [
26✔
242
            'valid' => empty($errors),
26✔
243
            'errors' => $errors,
26✔
244
            'warnings' => $warnings,
26✔
245
            'depth' => $depth
26✔
246
        ];
26✔
247
    }
248

249
    /**
250
     * Get loading statistics
251
     *
252
     * @return array Loading performance metrics
253
     */
254
    public function getLoadingStats(): array
255
    {
256
        return [
2✔
257
            'current_depth' => count($this->loadingStack),
2✔
258
            'loading_stack' => $this->loadingStack
2✔
259
        ];
2✔
260
    }
261

262
    /**
263
     * Reset loading state
264
     *
265
     * @return void
266
     */
267
    public function reset(): void
268
    {
269
        $this->loadingStack = [];
1✔
270
    }
271
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc