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

saygoweb / anorm / 18483557340

14 Oct 2025 02:25AM UTC coverage: 84.208% (+3.7%) from 80.483%
18483557340

Pull #42

github

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

845 of 960 new or added lines in 19 files covered. (88.02%)

66 existing lines in 3 files now uncovered.

1637 of 1944 relevant lines covered (84.21%)

15.67 hits per line

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

53.85
/src/Relationship/ManyHasMany.php
1
<?php
2

3
namespace Anorm\Relationship;
4

5
use Anorm\DataMapper;
6
use Anorm\Relationship\BatchLoader\ManyHasManyBatchLoader;
7

8
/**
9
 * Many-to-Many relationship
10
 * This model has many instances of another model through a join table
11
 */
12
class ManyHasMany extends Relationship
13
{
14
    /** @var string The join table name */
15
    protected $joinTable;
16

17
    /** @var string The foreign key in the join table for this model */
18
    protected $joinForeignKey;
19

20
    /** @var string The foreign key in the join table for the related model */
21
    protected $joinRelatedKey;
22

23
    public function __construct($relatedModelClass, $propertyName, $joinForeignKey, $joinRelatedKey, $joinTable, $primaryKey = 'id', $options = [])
24
    {
25
        parent::__construct($relatedModelClass, $propertyName, $joinForeignKey, $primaryKey, $options);
57✔
26
        $this->joinTable = $joinTable;
57✔
27
        $this->joinForeignKey = $joinForeignKey;
57✔
28
        $this->joinRelatedKey = $joinRelatedKey;
57✔
29
    }
30

31
    /**
32
     * Get the relationship type
33
     */
34
    public function getType()
35
    {
36
        return 'manyHasMany';
3✔
37
    }
38

39
    /**
40
     * Get the join table name
41
     */
42
    public function getJoinTable()
43
    {
44
        return $this->joinTable;
2✔
45
    }
46

47
    /**
48
     * Get the foreign key in the join table for this model
49
     */
50
    public function getJoinForeignKey()
51
    {
52
        return $this->joinForeignKey;
2✔
53
    }
54

55
    /**
56
     * Get the foreign key in the join table for the related model
57
     */
58
    public function getJoinRelatedKey()
59
    {
60
        return $this->joinRelatedKey;
2✔
61
    }
62

63
    /**
64
     * Load the related models for a many-to-many relationship
65
     *
66
     * @param object $sourceModel The model instance that owns the relationship
67
     * @param \PDO $pdo The database connection
68
     * @return array Array of related model instances
69
     */
70
    public function load($sourceModel, \PDO $pdo)
71
    {
72
        $relatedClass = $this->relatedModelClass;
2✔
73
        $sourceValue = $sourceModel->{$this->primaryKey};
2✔
74

75
        if ($sourceValue === null) {
2✔
76
            return [];
×
77
        }
78

79
        // Create an instance of the related model to get its mapper
80
        $relatedInstance = new $relatedClass($pdo);
2✔
81
        $mapper = $relatedInstance->_mapper;
2✔
82

83
        // Build the query to find related models through the join table
84
        // Use database column names directly
85
        $sql = "SELECT r.* FROM `{$mapper->table}` r
2✔
86
                INNER JOIN `{$this->joinTable}` j ON r.`{$this->primaryKey}` = j.`{$this->joinRelatedKey}`
2✔
87
                WHERE j.`{$this->joinForeignKey}` = ?";
2✔
88

89
        $result = $mapper->query($sql, [$sourceValue]);
2✔
90
        $relatedModels = [];
2✔
91

92
        while ($data = $result->fetch(\PDO::FETCH_ASSOC)) {
2✔
93
            $relatedModel = new $relatedClass($pdo);
1✔
94
            $relatedModel->_mapper->readArray($relatedModel, $data);
1✔
95
            $relatedModels[] = $relatedModel;
1✔
96
        }
97

98
        return $relatedModels;
2✔
99
    }
100

101
    /**
102
     * Generate JOIN clause for many-to-many relationship
103
     *
104
     * @param string $sourceTable The source table name
105
     * @param string $relatedTable The related table name
106
     * @return string The JOIN clause
107
     */
108
    public function generateJoinClause($sourceTable, $relatedTable)
109
    {
110
        $firstJoin = "LEFT JOIN `{$this->joinTable}` ON `{$sourceTable}`.`{$this->primaryKey}` = `{$this->joinTable}`.`{$this->joinForeignKey}`";
1✔
111
        $secondJoin = "LEFT JOIN `{$relatedTable}` ON `{$this->joinTable}`.`{$this->joinRelatedKey}` = `{$relatedTable}`.`{$this->primaryKey}`";
1✔
112
        return $firstJoin . ' ' . $secondJoin;
1✔
113
    }
114

115
    /**
116
     * Generate foreign key constraint SQL for ManyHasMany relationship
117
     * Creates foreign keys on the join table pointing to both source and target tables
118
     */
119
    public function generateForeignKeyConstraints($sourceTable)
120
    {
121
        $targetTable = $this->getTableNameFromModelClass($this->relatedModelClass);
×
122
        $constraints = [];
×
123

124
        // Foreign key from join table to source table
125
        $sourceConstraintName = "fk_{$this->joinTable}_{$this->joinForeignKey}";
×
126
        $onDelete = $this->constraintOptions['on_delete'];
×
127
        $onUpdate = $this->constraintOptions['on_update'];
×
128

129
        $constraints[] = "ALTER TABLE `{$this->joinTable}`
×
130
                         ADD CONSTRAINT `{$sourceConstraintName}`
×
131
                         FOREIGN KEY (`{$this->joinForeignKey}`)
×
132
                         REFERENCES `{$sourceTable}`(`{$this->primaryKey}`)
×
133
                         ON DELETE {$onDelete}
×
134
                         ON UPDATE {$onUpdate}";
×
135

136
        // Foreign key from join table to target table
137
        $targetConstraintName = "fk_{$this->joinTable}_{$this->joinRelatedKey}";
×
138

139
        $constraints[] = "ALTER TABLE `{$this->joinTable}`
×
140
                         ADD CONSTRAINT `{$targetConstraintName}`
×
141
                         FOREIGN KEY (`{$this->joinRelatedKey}`)
×
142
                         REFERENCES `{$targetTable}`(`{$this->primaryKey}`)
×
143
                         ON DELETE {$onDelete}
×
144
                         ON UPDATE {$onUpdate}";
×
145

146
        return $constraints;
×
147
    }
148

149
    /**
150
     * Generate SQL to create the join table if it doesn't exist
151
     */
152
    public function generateJoinTableSQL($sourceTable)
153
    {
154
        $targetTable = $this->getTableNameFromModelClass($this->relatedModelClass);
1✔
155

156
        $sql = "CREATE TABLE IF NOT EXISTS `{$this->joinTable}` (
1✔
157
                    `{$this->joinForeignKey}` INT(11) NOT NULL,
1✔
158
                    `{$this->joinRelatedKey}` INT(11) NOT NULL,
1✔
159
                    PRIMARY KEY (`{$this->joinForeignKey}`, `{$this->joinRelatedKey}`),
1✔
160
                    INDEX `idx_{$this->joinTable}_{$this->joinForeignKey}` (`{$this->joinForeignKey}`),
1✔
161
                    INDEX `idx_{$this->joinTable}_{$this->joinRelatedKey}` (`{$this->joinRelatedKey}`)
1✔
162
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
1✔
163

164
        return $sql;
1✔
165
    }
166

167
    /**
168
     * Load relationships for multiple source models in a single batch operation
169
     *
170
     * @param array $sourceModels Array of model instances that need relationships loaded
171
     * @param \PDO $pdo Database connection
172
     * @param array|null $fieldSelection Optional field selection for optimization
173
     * @return array Associative array of loaded relationship data, keyed by source model identifier
174
     */
175
    public function batchLoad(array $sourceModels, \PDO $pdo, ?array $fieldSelection = null): array
176
    {
NEW
177
        $batchLoader = new ManyHasManyBatchLoader();
×
NEW
178
        return $batchLoader->batchLoad($sourceModels, $this->propertyName, $fieldSelection);
×
179
    }
180

181
    /**
182
     * Distribute batch-loaded results to their corresponding source models
183
     *
184
     * @param array $sourceModels Array of model instances to receive the loaded data
185
     * @param array $batchResults Results from batchLoad(), keyed by source model identifier
186
     * @return void
187
     */
188
    public function distributeBatchResults(array $sourceModels, array $batchResults): void
189
    {
NEW
190
        $batchLoader = new ManyHasManyBatchLoader();
×
NEW
191
        $batchLoader->distributeBatchResults($sourceModels, $batchResults, $this->propertyName);
×
192
    }
193

194
    /**
195
     * Estimate the data size for this relationship with given parameters
196
     *
197
     * @param int $sourceCount Number of source models
198
     * @param array|null $fieldSelection Specific fields to load, or null for all fields
199
     * @return int Estimated data size in bytes
200
     */
201
    public function estimateDataSize(int $sourceCount, ?array $fieldSelection = null): int
202
    {
203
        // Estimate based on many-to-many cardinality
NEW
204
        $avgRelatedRecords = 3; // Conservative estimate for many-to-many
×
NEW
205
        $avgRecordSize = 1024; // 1KB per record estimate
×
206

NEW
207
        if ($fieldSelection !== null && !empty($fieldSelection)) {
×
208
            // Estimate size for selected fields only
NEW
209
            $avgRecordSize = count($fieldSelection) * 50; // 50 bytes per field estimate
×
210
        }
211

NEW
212
        return (int) ($sourceCount * $avgRelatedRecords * $avgRecordSize);
×
213
    }
214

215
    /**
216
     * Get the cardinality type of this relationship
217
     *
218
     * @return string One of: 'one-to-one', 'one-to-many', 'many-to-one', 'many-to-many'
219
     */
220
    public function getCardinality(): string
221
    {
NEW
222
        return 'many-to-many';
×
223
    }
224
}
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