Belongs To relation defines that an entity is owned by a related entity on an exclusive basis. Example: a post
belongs to an author, a comment belongs to a post. Most belongsTo relations can be created using the inverse option of
a declared hasOne or hasMany relation.
The child entity will always be persisted after its parent entity to ensure referential integrity.
To define a BelongsTo relation using the annotated entities extension:
use Cycle\Annotated\Annotation\Entity;
use Cycle\Annotated\Annotation\Column;
use Cycle\Annotated\Annotation\Relation\BelongsTo;
#[Entity]
class Post
{
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'string')]
private string $title;
#[BelongsTo(target: User::class)]
private User $author;
public function __construct(string $title, User $author)
{
$this->title = $title;
$this->author = $author;
}
public function getAuthor(): User
{
return $this->author;
}
public function setAuthor(User $author): void
{
$this->author = $author;
}
}
| Parameter | Type | Default | Description |
|---|---|---|---|
| target | string | - | Required. Target entity role or class name |
| load | string | 'lazy' | Loading strategy: 'lazy' or 'eager' |
| cascade | bool | true | Automatically save parent entity with child entity |
| nullable | bool | false | Whether relation can be null (child can exist without parent) |
| innerKey | string|array | null | Foreign key column(s) in child entity. Defaults to {relationName}_{outerKey} |
| outerKey | string|array | null | Key column(s) in parent entity. Defaults to parent's primary key |
| fkCreate | bool | true | Automatically create foreign key constraint |
| fkAction | string | 'CASCADE' | Foreign key action for both DELETE and UPDATE: 'CASCADE', 'NO ACTION', 'SET NULL' |
| fkOnDelete | string | null | Foreign key DELETE action (overrides fkAction if set): 'CASCADE', 'NO ACTION', 'SET NULL' |
| indexCreate | bool | true | Automatically create index on foreign key column(s) |
| inverse | Inverse | null | Configure inverse relation on parent entity. See Inverse Relations |
Since Cycle ORM v2.x,
innerKeyandouterKeycan be arrays for composite keys.
By default, the ORM will generate a foreign key column in the child entity using the pattern {relationPropertyName}_{parentPrimaryKey}:
#[Entity]
class Post
{
// Creates column: author_id (references user.id)
#[BelongsTo(target: User::class)]
private User $author;
}
You can customize column names explicitly:
#[Entity]
class Post
{
// Uses existing column: user_id
#[BelongsTo(target: User::class, innerKey: 'user_id')]
private User $author;
}
The ORM will automatically save the parent entity when persisting the child (unless cascade is set to false):
$user = new User("John Doe");
$post = new Post("My First Post", $user);
$manager = new \Cycle\ORM\EntityManager($orm);
$manager->persist($post);
$manager->run();
The save order ensures that:
User) is saved first, generating its IDPost) is saved second, with the parent's ID as the foreign keyYou can only set the relation to null if it's defined as nullable:
#[Entity]
class Post
{
#[BelongsTo(target: User::class, nullable: true)]
private ?User $author = null;
public function removeAuthor(): void
{
$this->author = null;
}
}
$post = $orm->getRepository(Post::class)->findByPK(1);
$post->removeAuthor();
$manager = new \Cycle\ORM\EntityManager($orm);
$manager->persist($post);
$manager->run(); // Sets author_id to NULL
Without nullable: true, attempting to set the relation to null will cause a database integrity constraint violation.
BelongsTo supports composite keys for complex relationships:
#[Entity]
class OrderItem
{
#[BelongsTo(
target: Order::class,
innerKey: ['order_id', 'order_date'],
outerKey: ['id', 'created_date']
)]
private Order $order;
}
Read more about composite keys.
To load related data, use the load method on your repository's Select query:
$post = $orm->getRepository(Post::class)
->select()
->load('author')
->wherePK(1)
->fetchOne();
print_r($post->getAuthor());
Configure the relation to always load with the entity:
#[BelongsTo(target: User::class, load: 'eager')]
private User $author;
With eager loading, the parent is automatically loaded without calling load():
$post = $orm->getRepository(Post::class)->findByPK(1);
// Author is already loaded
print_r($post->getAuthor());
$posts = $orm->getRepository(Post::class)
->select()
->load('author')
->load('category')
->load('comments')
->fetchAll();
with() for FilteringFilter the parent entity selection based on related entity criteria using with():
$posts = $orm->getRepository(Post::class)
->select()
->with('author')->where('author.status', 'active')
->fetchAll();
The Select query automatically joins related tables when you reference them in where() conditions:
// Automatically joins the user table
$posts = $orm->getRepository(Post::class)
->select()
->where('author.status', 'active')
->where('author.email', 'like', '%@example.com')
->fetchAll();
Combine multiple conditions and nested relations:
$posts = $orm->getRepository(Post::class)
->select()
->where('author.status', 'active')
->where('author.role', 'editor')
->where('published_at', '>', new DateTime('-30 days'))
->orderBy('author.name')
->fetchAll();
Define bidirectional relationships using the inverse parameter:
use Cycle\Annotated\Annotation\Relation\Inverse;
#[Entity]
class User
{
#[HasMany(
target: Post::class,
inverse: new Inverse(as: 'author', type: 'belongsTo')
)]
private array $posts = [];
}
This automatically creates the inverse BelongsTo relation on Post:
// Equivalent to defining on Post:
#[Entity]
class Post
{
#[BelongsTo(target: User::class)]
private User $author;
}
Inverse Attribute Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
| as | string | - | Required. Property name for the inverse relation |
| type | string | - | Required. Relation type: 'belongsTo', 'hasOne', 'hasMany', etc. |
| load | string | null | Loading strategy for inverse: 'eager', 'lazy', 'promise' |
By default, BelongsTo creates a foreign key with CASCADE on both DELETE and UPDATE:
#[BelongsTo(target: User::class)]
private User $author;
// Creates: FOREIGN KEY (author_id) REFERENCES users(id)
// ON DELETE CASCADE ON UPDATE CASCADE
Control what happens when the parent is deleted or updated:
#[BelongsTo(
target: User::class,
fkAction: 'SET NULL', // Both DELETE and UPDATE
nullable: true // Required for SET NULL
)]
private ?User $author;
Set different behavior for DELETE operations:
#[BelongsTo(
target: User::class,
fkAction: 'CASCADE', // Default for UPDATE
fkOnDelete: 'SET NULL', // Custom for DELETE
nullable: true // Required for SET NULL
)]
private ?User $author;
Available Actions:
| Action | Description |
|---|---|
| CASCADE | Delete/update child when parent is deleted/updated |
| SET NULL | Set foreign key to NULL (requires nullable: true) |
| NO ACTION | Prevent parent deletion/update if children exist (database enforced) |
For scenarios where you need manual control or cross-database relations:
#[BelongsTo(
target: User::class,
fkCreate: false,
indexCreate: false // Often combined with fkCreate: false
)]
private User $author;
Explicitly define the foreign key column:
#[Entity]
class Post
{
#[Column(type: 'primary')]
private int $id;
#[Column(type: 'integer', nullable: false)]
private int $author_id;
#[BelongsTo(target: User::class, innerKey: 'author_id')]
private User $author;
}
load()) instead of eager loading for better performance control#[Entity]
class Post
{
#[BelongsTo(target: User::class, nullable: true)]
private ?User $author = null;
public function getAuthor(): User
{
return $this->author ?? $this->getDefaultAuthor();
}
private function getDefaultAuthor(): User
{
// Return system user or guest user
}
}
#[Entity]
class Post
{
#[BelongsTo(target: User::class)]
private User $author;
public function changeAuthor(User $newAuthor): void
{
if ($this->author->getId() !== $newAuthor->getId()) {
// Log the change, notify, etc.
$this->author = $newAuthor;
}
}
}