Auto Discovering Relationship
The FieldAttribute has a parameter parentTable that is used to define the parent table of the field. This is used in
the case of a foreign key.
Once this attribute is set, the Repository can auto-discover the relationship between the tables
and generate the proper SQL with the relationship to retrieve the data.
How to use
You can use the parentTable parameter in the FieldAttribute to define the parent table of the field.
<?php
#[TableAttribute(tableName: 'table1')]
class Class1
{
#[FieldAttribute(primaryKey: true)]
public ?int $id;
}
#[TableAttribute(tableName: 'table2')]
class Class2
{
#[FieldAttribute(primaryKey: true)]
public ?int $id;
#[FieldAttribute(fieldName: "id_table1", parentTable: "table1")]
public ?int $idTable1;
}
$repository1 = new Repository($executor, Class1::class);
$repository2 = new Repository($executor, Class2::class);
This will automatically create the relationship between table1 and table2 through the ORM system's internal
relationship registry.
Generating Queries with Relationships
Once the relationships are registered, use Query::joinRelated() to add a
relationship-derived join to a query. It derives the ON condition from the
registered parentTable relationship instead of writing it by hand:
<?php
$query = Query::getInstance()
->table('table2')
->joinRelated('table1');
// SELECT * FROM table2 INNER JOIN table1 ON table1.id = table2.id_table1
joinRelated() appends the join to whichever table is already in the query (the base
table or a previously joined one), so it keeps your own base table. Because of that it
composes on top of an existing query — including one a Repository already scoped to
its table, where $repository->getByQuery($query) still returns that repository's
entity.
There are joinRelated(), leftJoinRelated() and rightJoinRelated(), mirroring
join()/leftJoin()/rightJoin(), so you can pick the join type.
If the target table is not directly related to a table already in the query, the intermediate tables on the shortest relationship path are joined automatically — you don't have to remember them (though keep in mind the extra joins have a cost). Tables already in the query are skipped, so chaining stays consistent:
<?php
// table1 -> table2 -> table4: table2 is joined for you.
$query = Query::getInstance()->table('table1')->joinRelated('table4');
// SELECT * FROM table1
// INNER JOIN table2 ON table1.id = table2.id_table1
// INNER JOIN table4 ON table2.id = table4.id_table2
// Naming the intermediate explicitly gives the same result (table2 is not joined twice):
$query = Query::getInstance()->table('table1')->joinRelated('table2')->joinRelated('table4');
For a left/rightJoinRelated() that spans intermediates, every hop it adds uses that
join type. joinRelated() throws an InvalidArgumentException only when no relationship
path connects the tables. For aliased joins, use join()/leftJoin()/rightJoin() with
an explicit ON.
Active Record models expose the same through Model::joinWith(...$tables), which starts
from the model's own table and joins the related ones.
Passing a model class (on-demand mapper registration)
joinRelated() (and joinWith()) accept a model class as well as a table name.
Relationship discovery walks the ORM's registry of mappers, and a mapper is only known
after it has been built. On a request that only touched one entity, the other entities'
mappers are not registered yet, so their relationships are invisible. Passing a class
fixes that: it registers that entity's mapper on demand — this reads the class attributes
(reflection) only and does not open a database connection — before resolving the join.
<?php
// 'project' was never referenced on this request, so its mapper is not registered.
$query = Query::getInstance()->table('task')->joinRelated(Project::class);
// Project's mapper is registered on demand, then:
// SELECT * FROM task INNER JOIN project ON project.id = task.project_id
To span a hidden intermediate, name each entity in the path — the same way Eloquent's
hasManyThrough names its through-model. Auto-discovery can only join an intermediate
whose mapper is registered, so naming it is what makes it known:
<?php
// note -> task -> project. A note has no project_id, so 'task' is the through-entity.
$notes = Note::joinWith(Task::class, Project::class)
->field('note.*')
->where('project.id = :id', ['id' => $projectId]);
// FROM note INNER JOIN task ON task.id = note.task_id
// INNER JOIN project ON project.id = task.project_id
ORM::getTableFromClass(Model::class) exposes the same resolution directly, returning the
table name and registering the mapper if needed.
Manual Relationship Definition
If you need to define relationships manually (without using attributes), you can use the addRelationship method:
<?php
ORM::addRelationship("table1", "table2", "id_table1", "id");
This defines a relationship where table2.id_table1 is a foreign key that references table1.id.
You can also use Mapper objects:
<?php
$mapper1 = new Mapper(Class1::class);
$mapper2 = new Mapper(Class2::class);
ORM::addRelationship($mapper1, $mapper2, "id_table1");
In this case, the primary key will be automatically determined from the first mapper.
Getting Relationship Information
You can get information about relationships between tables:
<?php
// Get relationship keys
$relationships = ORM::getRelationship("table1", "table2");
// Get detailed relationship data
$relationshipData = ORM::getRelationshipData("table1", "table2");
Return Value of getRelationship
The ORM::getRelationship() method returns an array of relationship keys that define the path between the specified
tables. Each key in the array is a string formatted as "tableA,tableB" which identifies a specific relationship in the
internal relationship registry.
For example, if you call:
$relationships = ORM::getRelationship("table1", "table2");
It might return something like:
["table1,table2"]
If there are multiple tables involved in a path (like when finding relationships between more distant tables), it would return all the relationship keys in the path, like:
["table1,intermediateTable", "intermediateTable,table2"]
These keys identify the relationships in the internal registry and are used to retrieve the actual relationship details.
Return Value of getRelationshipData
The ORM::getRelationshipData() method returns the actual detailed relationship information for the tables. It takes
the keys returned by getRelationship() and retrieves the corresponding relationship data objects.
The method returns an array of relationship data objects, where each object contains:
parent: The parent table namechild: The child table namepk: The primary key in the parent tablefk: The foreign key in the child table
For example:
$relationshipData = ORM::getRelationshipData("table1", "table2");
Might return:
[
[
"parent" => "table1",
"child" => "table2",
"pk" => "id",
"fk" => "id_table1"
]
]
This detailed information is used internally when automatically constructing joins in queries between the tables.
Limitations
- This feature does not support multiple relationships between the same tables
- Primary keys with two or more fields are not fully supported for auto-relationship discovery