From f10373d301515748618d2bb861f6ff84e3ae3808 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 27 Feb 2024 16:12:44 +0800 Subject: [PATCH 01/18] Initial edits --- database/relations.md | 126 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/database/relations.md b/database/relations.md index 6e1890dd..759f8d7a 100644 --- a/database/relations.md +++ b/database/relations.md @@ -4,36 +4,52 @@ Database tables are often related to one another. For example, a blog post may have many comments, or an order could be related to the user who placed it. Winter makes managing and working with these relationships easy and supports several different types of relationships. -> **NOTE:** If you are selecting specific columns in your query and want to load relationships as well, you need to make sure that the columns that contain the keying data (i.e. `id`, `foreign_key`, etc) are included in your select statement. Otherwise, Winter cannot connect the relations. - ## Defining relationships -Model relationships are defined as properties on your model classes. An example of defining relationships: +Winter provides two methods of defining model relationships. Both provide the same level of functionality - which one you use is entirely up to your own preferences. + +- Property-based array configuration in the model class, available in all versions of Winter CMS (Property style) +- Relation methods defined in the model class, synonymous with Laravel, available since Winter CMS v1.2.5 (Method style) + +### Property style relationship definition + +Model relationships can be defined as properties on your model classes. An example of defining relationships: ```php class User extends Model { + public $hasOne = [ + 'profile' => 'Acme\Blog\Models\Profile', + ]; + public $hasMany = [ - 'posts' => 'Acme\Blog\Models\Post' - ] + 'posts' => 'Acme\Blog\Models\Post', + ]; } ``` -Relationships like models themselves, also serve as powerful [query builders](query), accessing relationships as functions provides powerful method chaining and querying capabilities. For example: +Winter CMS automatically converts relations defined in this way into method endpoints on the model. For example, the above `posts` relation can be accessed by calling the `posts()` method on an instance of the `User` model. + +```php +$user->posts() +``` + +Relationships, like the models themselves, also serve as powerful [query builders](query) to allow accessing relationships as functions provides powerful method chaining and querying capabilities. For example: ```php $user->posts()->where('is_active', true)->get(); ``` -Accessing a relationship as a property is also possible: +Accessing a relationship as a property is also possible. Retrieving the relation as a property will give you the "value" of the relation. This means that a single-record relation (ie. `hasOne`) will provide you with the related model directly. In the case of a multiple-record relation (ie. `hasMany`), a collection is usually returned that contains all related records. ```php -$user->posts; +$user->profile; // The "Acme\Blog\Models\Profile" record attached to this user +$user->posts; // A collection containing every "Acme\Blog\Models\Post" record attached to this user ``` > **NOTE**: All relationship queries have [in-memory caching enabled](../database/query#in-memory-caching) by default. The `load($relation)` method won't force cache to flush. To reload the memory cache use the `reloadRelations()` or the `reload()` methods on the model object. -### Detailed definitions +### Detailed property style definitions Each definition can be an array where the key is the relation name and the value is a detail array. The detail array's first value is always the related model class name and all other values are parameters that must have a key name. @@ -98,6 +114,98 @@ public $belongsToMany = [ ]; ``` +### Method style relation definition + +> **Note:** Method style relation definition is available from Winter v1.2.5. + +Model relations can also be defined as methods in a model class, similar to the base Laravel framework. + +```php +class User extends Model +{ + public function profile(): HasOne + { + return $this->hasOne('Acme\Blog\Models\Profile'); + } + + public function posts(): HasMany + { + return $this->hasMany('Acme\Blog\Models\Post'); + } +} +``` + +This way provides a more familiar syntax for Laravel users, and allows for code editors and IDEs to provide syntax support for relations. + +One key difference between property style definitions and method style definitions is that a relation method must *explicitly* be defined as a relation method in order for it to be picked up in certain circumstances, for example, when determining all available relations for cascading deletions. + +In order to define a relation method, the method must have a single return type that matches one of the `Winter\Storm\Database\Relations` classes: + +```php +use Winter\Storm\Database\Relations\HasMany; + +public function posts(): HasMany +{ + return $this->hasMany('Acme\Blog\Models\Post'); +} +``` + +Alternatively, you may also use the `Relation` attribute on the method to define it is as a relation method: + +```php +use Winter\Storm\Database\Attributes\Relation; + +#[Relation] +public function posts() +{ + return $this->hasMany('Acme\Blog\Models\Post'); +} +``` + +Since the relation method is already a method, you may call the method to retrieve the relation, similar to the property style relations. This returns a [query builder](query) for the relation and allows powerful chaining capabilities. + +```php +$user->posts()->where('is_active', true)->get(); +``` + +As with the property style relations, you can also call the relation as a property of the model, which returns the relation "value". This means that a single-record relation (ie. `hasOne`) will provide you with the related model directly. In the case of a multiple-record relation (ie. `hasMany`), a collection is usually returned that contains all related records. + +```php +$user->profile; // The "Acme\Blog\Models\Profile" record attached to this user +$user->posts; // A collection containing every "Acme\Blog\Models\Post" record attached to this user +``` + +### Detailed relation methods + +Another key difference between property style and method style relation definitions lies in the additional parameters that may be applied to the relation. With the property style, you can configure the relation by providing additional keys and values in the relation configuration array. With the method style, you define these parameters by using chained methods, for example: + +```php +public function posts(): HasMany +{ + return $this->hasMany('Acme\Blog\Models\Post')->dependent()->pushable(); +} +``` + +The following chained methods are available to define additional parameters about the relation: + +Method | Description +------ | ----------- +`->dependent()` | Makes this relation "dependent" on the primary model. The related model records will be deleted when the primary model is deleted. This is only available for the following relation types: `attachOne`, `attachMany`, `hasOne`, `hasMany`, `morphOne` and `morphMany`. +`->notDependent()` | Makes this relation independent of the primary model. The related model records will not be deleted when the primary model is deleted. This is the default behavior. +`->detachable()` | Makes this relation detach from the primary model if the primary model is deleted or the relationship is broken. This is the default behavior, and is only available, for the following relation types: `belongsToMany`, `morphToMany` and `morphedByMany`. +`->notDetachable()` | Prevents the relation from detaching from the primary model when the primary model is deleted or the relationship is broken. +`->pushable()` | Sets this relation to save when `push()` is run on the primary model. This is the default behavior. +`->notPushable()` | Sets this relation to not be saved when `push()` is run on the primary model. + +You might have noticed that there are no chain methods for handling "constraint" options like the `order`, `conditions` and `scope` options available in the property style relations. That is because relations defined in this format are already [query builders](query) - you can simply add the constraints directly to the relation! + +```php +public function posts(): HasMany +{ + return $this->hasMany('Acme\Blog\Models\Post')->dependent()->published()->where('free_article', true); +} +``` + ## Relationship types The following relations types are available: From b78ea5493ecfcf5960e6e7df8d73a44573e34d4d Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Wed, 28 Feb 2024 08:16:23 +0800 Subject: [PATCH 02/18] Update database/relations.md Co-authored-by: Marc Jauvin <marc.jauvin@gmail.com> --- database/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index 759f8d7a..b89d41f0 100644 --- a/database/relations.md +++ b/database/relations.md @@ -150,7 +150,7 @@ public function posts(): HasMany } ``` -Alternatively, you may also use the `Relation` attribute on the method to define it is as a relation method: +Alternatively, you may also use the `Relation` attribute on the method to define it as a relation method: ```php use Winter\Storm\Database\Attributes\Relation; From 9639ff674616126756c9de51645bf11c52ad1fad Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Wed, 28 Feb 2024 08:42:59 +0800 Subject: [PATCH 03/18] Add examples for all types of method relations --- database/relations.md | 213 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 3 deletions(-) diff --git a/database/relations.md b/database/relations.md index b89d41f0..4eee59be 100644 --- a/database/relations.md +++ b/database/relations.md @@ -228,9 +228,16 @@ use Model; class User extends Model { + // Property style public $hasOne = [ 'phone' => 'Acme\Blog\Models\Phone' ]; + + // Method style + public function phone(): HasOne + { + return $this->hasOne('Acme\Blog\Models\Phone'); + } } ``` @@ -243,17 +250,31 @@ $phone = User::find(1)->phone; The model assumes the foreign key of the relationship based on the model name. In this case, the `Phone` model is automatically assumed to have a `user_id` foreign key. If you wish to override this convention, you may pass the `key` parameter to the definition: ```php +// Property style public $hasOne = [ 'phone' => ['Acme\Blog\Models\Phone', 'key' => 'my_user_id'] ]; + +// Method style +public function phone(): HasOne +{ + return $this->hasOne('Acme\Blog\Models\Phone', 'my_user_id'); +} ``` Additionally, the model assumes that the foreign key should have a value matching the `id` column of the parent. In other words, it will look for the value of the user's `id` column in the `user_id` column of the `Phone` record. If you would like the relationship to use a value other than `id`, you may pass the `otherKey` parameter to the definition: ```php +// Property style public $hasOne = [ 'phone' => ['Acme\Blog\Models\Phone', 'key' => 'my_user_id', 'otherKey' => 'my_id'] ]; + +// Method style +public function phone(): HasOne +{ + return $this->hasOne('Acme\Blog\Models\Phone', 'my_user_id', 'my_id'); +} ``` #### Defining the inverse of a One To One relation @@ -263,26 +284,47 @@ Now that we can access the `Phone` model from our `User`. Let's do the opposite ```php class Phone extends Model { + // Property style public $belongsTo = [ 'user' => 'Acme\Blog\Models\User' ]; + + // Method style + public function user(): BelongsTo + { + return $this->belongsTo('Acme\Blog\Models\User'); + } } ``` In the example above, the model will try to match the `user_id` from the `Phone` model to an `id` on the `User` model. It determines the default foreign key name by examining the name of the relationship definition and suffixing the name with `_id`. However, if the foreign key on the `Phone` model is not `user_id`, you may pass a custom key name using the `key` parameter on the definition: ```php +// Property style public $belongsTo = [ 'user' => ['Acme\Blog\Models\User', 'key' => 'my_user_id'] ]; + +// Method style +public function user(): BelongsTo +{ + return $this->belongsTo('Acme\Blog\Models\User', 'my_user_id'); +} ``` If your parent model does not use `id` as its primary key, or you wish to join the child model to a different column, you may pass the `otherKey` parameter to the definition specifying your parent table's custom key: ```php +// Property style public $belongsTo = [ 'user' => ['Acme\Blog\Models\User', 'key' => 'my_user_id', 'otherKey' => 'my_id'] ]; + +// Method style +public function user(): BelongsTo +{ + return $this->belongsTo('Acme\Blog\Models\User', 'my_user_id', 'my_id'); +} ``` #### Default models @@ -313,9 +355,16 @@ A one-to-many relationship is used to define relationships where a single model ```php class Post extends Model { + // Property style public $hasMany = [ 'comments' => 'Acme\Blog\Models\Comment' ]; + + // Method style + public function comments(): HasMany + { + return $this->hasMany('Acme\Blog\Models\Comment'); + } } ``` @@ -340,9 +389,16 @@ $comments = Post::find(1)->comments()->where('title', 'foo')->first(); Like the `hasOne` relation, you may also override the foreign and local keys by passing the `key` and `otherKey` parameters on the definition respectively: ```php +// Property style public $hasMany = [ 'comments' => ['Acme\Blog\Models\Comment', 'key' => 'my_post_id', 'otherKey' => 'my_id'] ]; + +// Method style +public function comments(): HasMany +{ + return $this->hasMany('Acme\Blog\Models\Comment', 'my_post_id', 'my_id'); +} ``` #### Defining the inverse of a One To Many relation @@ -352,9 +408,16 @@ Now that we can access all of a post's comments, let's define a relationship to ```php class Comment extends Model { + // Property style public $belongsTo = [ 'post' => 'Acme\Blog\Models\Post' ]; + + // Method style + public function post(): BelongsTo + { + return $this->belongsTo('Acme\Blog\Models\Post'); + } } ``` @@ -369,17 +432,31 @@ echo $comment->post->title; In the example above, the model will try to match the `post_id` from the `Comment` model to an `id` on the `Post` model. It determines the default foreign key name by examining the name of the relationship and suffixing it with `_id`. However, if the foreign key on the `Comment` model is not `post_id`, you may pass a custom key name using the `key` parameter: ```php +// Property style public $belongsTo = [ 'post' => ['Acme\Blog\Models\Post', 'key' => 'my_post_id'] ]; + +// Method style +public function post(): BelongsTo +{ + return $this->belongsTo('Acme\Blog\Models\Post', 'my_post_id'); +} ``` If your parent model does not use `id` as its primary key, or you wish to join the child model to a different column, you may pass the `otherKey` parameter to the definition specifying your parent table's custom key: ```php +// Property style public $belongsTo = [ 'post' => ['Acme\Blog\Models\Post', 'key' => 'my_post_id', 'otherKey' => 'my_id'] ]; + +// Method style +public function post(): BelongsTo +{ + return $this->belongsTo('Acme\Blog\Models\Post', 'my_post_id', 'my_id'); +} ``` ### Many To Many @@ -402,9 +479,16 @@ Many-to-many relationships are defined adding an entry to the `$belongsToMany` p ```php class User extends Model { + // Property style public $belongsToMany = [ 'roles' => 'Acme\Blog\Models\Role' ]; + + // Method style + public function roles(): BelongsToMany + { + return $this->belongsToMany('Acme\Blog\Models\Role'); + } } ``` @@ -427,14 +511,22 @@ $roles = User::find(1)->roles()->orderBy('name')->get(); As mentioned previously, to determine the table name of the relationship's joining table, the model will join the two related model names in alphabetical order. However, you are free to override this convention. You may do so by passing the `table` parameter to the `belongsToMany` definition: ```php +// Property style public $belongsToMany = [ 'roles' => ['Acme\Blog\Models\Role', 'table' => 'acme_blog_role_user'] ]; + +// Method style +public function roles(): BelongsToMany +{ + return $this->belongsToMany('Acme\Blog\Models\Role', 'acme_blog_role_user'); +} ``` In addition to customizing the name of the joining table, you may also customize the column names of the keys on the table by passing additional parameters to the `belongsToMany` definition. The `key` parameter is the foreign key name of the model on which you are defining the relationship, while the `otherKey` parameter is the foreign key name of the model that you are joining to: ```php +// Property style public $belongsToMany = [ 'roles' => [ 'Acme\Blog\Models\Role', @@ -443,6 +535,12 @@ public $belongsToMany = [ 'otherKey' => 'my_role_id' ] ]; + +// Method style +public function roles(): BelongsToMany +{ + return $this->belongsToMany('Acme\Blog\Models\Role', 'acme_blog_role_user', 'my_user_id', 'my_role_id'); +} ``` #### Defining the inverse of the relationship @@ -452,9 +550,16 @@ To define the inverse of a many-to-many relationship, you simply place another ` ```php class Role extends Model { + // Property style public $belongsToMany = [ 'users' => 'Acme\Blog\Models\User' ]; + + // Method style + public function users(): BelongsToMany + { + return $this->belongsToMany('Acme\Blog\Models\User'); + } } ``` @@ -477,20 +582,34 @@ Notice that each `Role` model we retrieve is automatically assigned a `pivot` at By default, only the model keys will be present on the `pivot` object. If your pivot table contains extra attributes, you must specify them when defining the relationship: ```php +// Property style public $belongsToMany = [ - 'roles' => [ - 'Acme\Blog\Models\Role', + 'users' => [ + 'Acme\Blog\Models\User', 'pivot' => ['column1', 'column2'] ] ]; + +// Method style +public function users(): BelongsToMany +{ + return $this->belongsToMany('Acme\Blog\Models\User')->withPivot('column1', 'column2'); +} ``` If you want your pivot table to have automatically maintained `created_at` and `updated_at` timestamps, use the `timestamps` parameter on the relationship definition: ```php +// Property style public $belongsToMany = [ - 'roles' => ['Acme\Blog\Models\Role', 'timestamps' => true] + 'users' => ['Acme\Blog\Models\User', 'timestamps' => true] ]; + +// Method style +public function users(): BelongsToMany +{ + return $this->belongsToMany('Acme\Blog\Models\User')->withTimestamps(); +} ``` These are the parameters supported for `belongsToMany` relations: @@ -533,12 +652,19 @@ Now that we have examined the table structure for the relationship, let's define ```php class Country extends Model { + // Property style public $hasManyThrough = [ 'posts' => [ 'Acme\Blog\Models\Post', 'through' => 'Acme\Blog\Models\User' ], ]; + + // Method style + public function posts(): HasManyThrough + { + return $this->hasManyThrough('Acme\Blog\Models\Post', 'Acme\Blog\Models\User'); + } } ``` @@ -547,6 +673,7 @@ The first argument passed to the `$hasManyThrough` relation is the name of the f Typical foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the `key`, `otherKey` and `throughKey` parameters to the `$hasManyThrough` definition. The `key` parameter is the name of the foreign key on the intermediate model, the `throughKey` parameter is the name of the foreign key on the final model, while the `otherKey` is the local key. ```php +// Property style public $hasManyThrough = [ 'posts' => [ 'Acme\Blog\Models\Post', @@ -556,6 +683,12 @@ public $hasManyThrough = [ 'otherKey' => 'my_id' ], ]; + +// Method style +public function posts(): HasManyThrough +{ + return $this->hasManyThrough('Acme\Blog\Models\Post', 'Acme\Blog\Models\User', 'my_country_id', 'my_user_id', 'my_id'); +} ``` ### Has One Through @@ -580,12 +713,19 @@ Though the `history` table does not contain a `supplier_id` column, the `hasOneT ```php class Supplier extends Model { + // Property style public $hasOneThrough = [ 'userHistory' => [ 'Acme\Supplies\Model\History', 'through' => 'Acme\Supplies\Model\User' ], ]; + + // Method style + public function userHistory(): HasOneThrough + { + return $this->hasOneThrough('Acme\Supplies\Model\History', 'Acme\Supplies\Model\User'); + } } ``` @@ -594,6 +734,7 @@ The first array parameter passed to the `$hasOneThrough` property is the name of Typical foreign key conventions will be used when performing the relationship's queries. If you would like to customize the keys of the relationship, you may pass them as the `key`, `otherKey` and `throughKey` parameters to the `$hasManyThrough` definition. The `key` parameter is the name of the foreign key on the intermediate model, the `throughKey` parameter is the name of the foreign key on the final model, while the `otherKey` is the local key. ```php +// Property style public $hasOneThrough = [ 'userHistory' => [ 'Acme\Supplies\Model\History', @@ -603,6 +744,12 @@ public $hasOneThrough = [ 'otherKey' => 'id' ], ]; + +// Method style +public function userHistory(): HasOneThrough +{ + return $this->hasOneThrough('Acme\Supplies\Model\History', 'Acme\Supplies\Model\User', 'supplier_id', 'user_id', 'id'); +} ``` ### Polymorphic relations @@ -636,23 +783,44 @@ Next, let's examine the model definitions needed to build this relationship: ```php class Photo extends Model { + // Property style public $morphTo = [ 'imageable' => [] ]; + + // Method style + public function imageable(): MorphTo + { + return $this->morphTo(); + } } class Staff extends Model { + // Property style public $morphOne = [ 'photo' => ['Acme\Blog\Models\Photo', 'name' => 'imageable'] ]; + + // Method style + public function photo(): MorphOne + { + return $this->morphOne('Acme\Blog\Models\Photo', 'imageable'); + } } class Product extends Model { + // Property style public $morphOne = [ 'photo' => ['Acme\Blog\Models\Photo', 'name' => 'imageable'] ]; + + // Method style + public function photo(): MorphOne + { + return $this->morphOne('Acme\Blog\Models\Photo', 'imageable'); + } } ``` @@ -703,23 +871,44 @@ Next, let's examine the model definitions needed to build this relationship: ```php class Comment extends Model { + // Property style public $morphTo = [ 'commentable' => [] ]; + + // Method style + public function commentable(): MorphTo + { + return $this->morphTo(); + } } class Post extends Model { + // Property style public $morphMany = [ 'comments' => ['Acme\Blog\Models\Comment', 'name' => 'commentable'] ]; + + // Method style + public function comments(): MorphMany + { + return $this->morphMany('Acme\Blog\Models\Comment', 'commentable'); + } } class Product extends Model { + // Property style public $morphMany = [ 'comments' => ['Acme\Blog\Models\Comment', 'name' => 'commentable'] ]; + + // Method style + public function comments(): MorphMany + { + return $this->morphMany('Acme\Blog\Models\Comment', 'commentable'); + } } ``` @@ -783,9 +972,16 @@ Next, we're ready to define the relationships on the model. The `Post` and `Vide ```php class Post extends Model { + // Property style public $morphToMany = [ 'tags' => ['Acme\Blog\Models\Tag', 'name' => 'taggable'] ]; + + // Method style + public function tags(): MorphToMany + { + return $this->morphToMany('Acme\Blog\Models\Tag', 'taggable'); + } } ``` @@ -796,10 +992,21 @@ Next, on the `Tag` model, you should define a relation for each of its related m ```php class Tag extends Model { + // Property style public $morphedByMany = [ 'posts' => ['Acme\Blog\Models\Post', 'name' => 'taggable'], 'videos' => ['Acme\Blog\Models\Video', 'name' => 'taggable'] ]; + + // Method style + public function posts(): MorphedByMany + { + return $this->morphedByMany('Acme\Blog\Models\Post', 'taggable'); + } + public function videos(): MorphedByMany + { + return $this->morphedByMany('Acme\Blog\Models\Video', 'taggable'); + } } ``` From 0c96f5d346ca14772fd346ae90910fc1cc63ef37 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Wed, 28 Feb 2024 09:57:41 +0800 Subject: [PATCH 04/18] Update Soft Delete trait docs --- database/traits.md | 71 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/database/traits.md b/database/traits.md index 40bf3365..1b8d410e 100644 --- a/database/traits.md +++ b/database/traits.md @@ -526,7 +526,9 @@ You can also create custom validation rules the [same way](../services/validatio ## Soft deleting -When soft deleting a model, it is not actually removed from your database. Instead, a `deleted_at` timestamp is set on the record. To enable soft deletes for a model, apply the `Winter\Storm\Database\Traits\SoftDelete` trait to the model and add the deleted_at column to your `$dates` property: +Soft deleting allows for models to "act" as being deleted whilst still remaining in the database. Instead of removing a model record from the database on delete, a `deleted_at` timestamp is set on the record which, by default, will hide the record from any database query results. + +To enable soft deleting for a model, apply the `Winter\Storm\Database\Traits\SoftDelete` trait to the model and add the `deleted_at` column to your `$dates` property: ```php class User extends Model @@ -555,6 +557,19 @@ if ($user->trashed()) { } ``` +If you wish, you may also change the column that the deleted timestamp is saved to, by specifying a `DELETED_AT` constant in your model. Make sure that you change the column in the `$dates` property as well. + +```php +class User extends Model +{ + // ... + const DELETED_AT = 'hidden_at'; + + protected $dates = ['hidden_at']; + // ... +} +``` + ### Querying soft deleted models #### Including soft deleted models @@ -611,7 +626,22 @@ $user->posts()->forceDelete(); ### Soft deleting relations -When two related models have soft deletes enabled, you can cascade the delete event by defining the `softDelete` option in the [relation definition](relations#detailed-definitions). In this example, if the user model is soft deleted, the comments belonging to that user will also be soft deleted. +A model that uses the `SoftDelete` trait may also define that related models also be soft deleting when the primary model is soft delete. You can cascade the soft deletion by defining the `softDelete` option in the [relation definition](relations#detailed-definitions) if you are using property-style relation definitions. + +In this example, if the user model is soft deleted, the comments belonging to that user will also be soft deleted. + +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\SoftDelete; + + public $hasMany = [ + 'comments' => ['Acme\Blog\Models\Comment', 'softDelete' => true], + ]; +} +``` + +If your related model uses a different column for storing the deleted timestamp, you may specify it in the `deletedAtColumn` option on the relation definition. ```php class User extends Model @@ -619,13 +649,44 @@ class User extends Model use \Winter\Storm\Database\Traits\SoftDelete; public $hasMany = [ - 'comments' => ['Acme\Blog\Models\Comment', 'softDelete' => true] + 'comments' => [ + 'Acme\Blog\Models\Comment', + 'softDelete' => true, + 'deletedAtColumn' => 'hidden_at', + ], ]; } ``` -> **NOTE:** If the soft deleting relation is using a pivot table, you can set the `deletedAtColumn` option on the relation definition to change the column that will hold the soft deletion date in the pivot table, otherwise, it defaults to `deleted_at`. -> **NOTE:** If the related model does not use the soft delete trait, it will be treated the same as the `delete` option and deleted permanently. +If you use method style relations, you can include the `->softDeletable()` chained method to the relation definition to indicate that this relation should also be soft deleted when the main model is soft deleted. + +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\SoftDelete; + + public function comments(): HasMany + { + $this->hasMany('Acme\Blog\Models\Comment')->softDeletable(); + } +} +``` + +If your related model uses a different column for storing the deleted timestamp, you may specify it in the first parameter of the chained method: + +```php +class User extends Model +{ + use \Winter\Storm\Database\Traits\SoftDelete; + + public function comments(): HasMany + { + $this->hasMany('Acme\Blog\Models\Comment')->softDeletable('hidden_at'); + } +} +``` + +> **WARNING:** If the related model does not also use the `SoftDelete` trait and you specify the relation as soft-deletable, the relation will be *permanently* deleted. Under these same conditions, when the primary model is restored, all the related models that use the `softDelete` option will also be restored. From e0f4393e84b115ab28488e93a83edcf0a3c3dfc6 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Fri, 1 Mar 2024 16:24:52 +0800 Subject: [PATCH 05/18] Update relations.md --- database/relations.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index 4eee59be..d20f731c 100644 --- a/database/relations.md +++ b/database/relations.md @@ -200,12 +200,15 @@ Method | Description You might have noticed that there are no chain methods for handling "constraint" options like the `order`, `conditions` and `scope` options available in the property style relations. That is because relations defined in this format are already [query builders](query) - you can simply add the constraints directly to the relation! ```php -public function posts(): HasMany +#[Relation] +public function posts() { return $this->hasMany('Acme\Blog\Models\Post')->dependent()->published()->where('free_article', true); } ``` +> **Note:** If your relation is constrained in this fashion, the object returned will be a query builder, not the original relation class. You will need to use the `Relation` attribute to mark methods that return query builders as a relation method. + ## Relationship types The following relations types are available: From a3801a9fa5232e7ea95ccf1dd4614159c7aebeb0 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Mon, 29 Jul 2024 14:30:40 +0800 Subject: [PATCH 06/18] Add default models example for method style --- database/relations.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index d20f731c..e7d86cce 100644 --- a/database/relations.md +++ b/database/relations.md @@ -332,7 +332,7 @@ public function user(): BelongsTo #### Default models -The `belongsTo` relationship lets you define a default model that will be returned if the given relationship is `null`. This pattern is often referred to as the [Null Object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) and can help remove conditional checks in your code. In the following example, the `user` relation will return an empty `Acme\Blog\Models\User` model if no `user` is attached to the post: +The `belongsTo`, `hasOne`, `hasOneThrough` and `morphOne` relationships allow you to define a default model that will be returned if the given relationship is `null`. This pattern is often referred to as the [Null Object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) and can help remove conditional checks in your code. In the following example, the `user` relation will return an empty `Acme\Blog\Models\User` model if no `user` is attached to the post: ```php public $belongsTo = [ @@ -351,6 +351,24 @@ public $belongsTo = [ ]; ``` +If you have defined the relation as a method, you may use the `withDefault()` method to define a default model: + +```php +public function user(): BelongsTo +{ + return $this->belongsTo('Acme\Blog\Models\User')->withDefault(); +} + +// With attributes + +public function user(): BelongsTo +{ + return $this->belongsTo('Acme\Blog\Models\User')->withDefault([ + 'name' => 'Guest', + ]); +} +``` + ### One To Many A one-to-many relationship is used to define relationships where a single model owns any amount of other models. For example, a blog post may have an infinite number of comments. Like all other relationships, one-to-many relationships are defined adding an entry to the `$hasMany` property on your model: From 70c3ae9bad71128ba036bc1c9c6ddcf2bdff54fb Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 30 Jul 2024 13:09:35 +0800 Subject: [PATCH 07/18] Update database/relations.md Co-authored-by: Luke Towers <github@luketowers.ca> --- database/relations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/relations.md b/database/relations.md index e7d86cce..39952e29 100644 --- a/database/relations.md +++ b/database/relations.md @@ -8,8 +8,8 @@ Database tables are often related to one another. For example, a blog post may h Winter provides two methods of defining model relationships. Both provide the same level of functionality - which one you use is entirely up to your own preferences. -- Property-based array configuration in the model class, available in all versions of Winter CMS (Property style) -- Relation methods defined in the model class, synonymous with Laravel, available since Winter CMS v1.2.5 (Method style) +- Property-based array configuration in the model class (Property style) +- Relation methods defined in the model class, [synonymous with Laravel](https://laravel.com/docs/9.x/eloquent-relationships#defining-relationships) (Method style) ### Property style relationship definition From 77e17a55f02462e99e207804fb84be15623c765f Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 30 Jul 2024 13:09:50 +0800 Subject: [PATCH 08/18] Update database/relations.md Co-authored-by: Luke Towers <github@luketowers.ca> --- database/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index 39952e29..f226c025 100644 --- a/database/relations.md +++ b/database/relations.md @@ -19,7 +19,7 @@ Model relationships can be defined as properties on your model classes. An examp class User extends Model { public $hasOne = [ - 'profile' => 'Acme\Blog\Models\Profile', + 'profile' => \Acme\Blog\Models\Profile::class, ]; public $hasMany = [ From 397cf4a518b468f6dae68c2c35d28eeee838125c Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 30 Jul 2024 13:15:22 +0800 Subject: [PATCH 09/18] Update database/relations.md Co-authored-by: Luke Towers <github@luketowers.ca> --- database/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index f226c025..503f6fe8 100644 --- a/database/relations.md +++ b/database/relations.md @@ -207,7 +207,7 @@ public function posts() } ``` -> **Note:** If your relation is constrained in this fashion, the object returned will be a query builder, not the original relation class. You will need to use the `Relation` attribute to mark methods that return query builders as a relation method. +> **NOTE:** If your relation is constrained in this fashion, the object returned will be a query builder, not the original relation class. You will need to use the `Relation` attribute to mark methods that return query builders as a relation method. ## Relationship types From cd02a0da3fd73f53aede9589c3da848db670ade8 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 30 Jul 2024 13:17:58 +0800 Subject: [PATCH 10/18] Update database/relations.md Co-authored-by: Luke Towers <github@luketowers.ca> --- database/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index 503f6fe8..42f3cd51 100644 --- a/database/relations.md +++ b/database/relations.md @@ -332,7 +332,7 @@ public function user(): BelongsTo #### Default models -The `belongsTo`, `hasOne`, `hasOneThrough` and `morphOne` relationships allow you to define a default model that will be returned if the given relationship is `null`. This pattern is often referred to as the [Null Object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) and can help remove conditional checks in your code. In the following example, the `user` relation will return an empty `Acme\Blog\Models\User` model if no `user` is attached to the post: +The `belongsTo`, `hasOne`, `hasOneThrough`, and `morphOne` relationships allow you to define a default model that will be returned if the given relationship is `null`. This pattern is often referred to as the [Null Object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) and can help remove conditional checks in your code. In the following example, the `user` relation will return an empty `Acme\Blog\Models\User` model if no `user` is attached to the post: ```php public $belongsTo = [ From 89a696f75ee9108cfb3c063e893da4101ecaf1d6 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Sat, 28 Sep 2024 11:57:26 +0800 Subject: [PATCH 11/18] Update database/relations.md Co-authored-by: Luke Towers <github@luketowers.ca> --- database/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index 42f3cd51..ee727de2 100644 --- a/database/relations.md +++ b/database/relations.md @@ -23,7 +23,7 @@ class User extends Model ]; public $hasMany = [ - 'posts' => 'Acme\Blog\Models\Post', + 'posts' => \Acme\Blog\Models\Post::class, ]; } ``` From b342c9cae3fa8e9fc3b2fd2e3cb3b6adf1e10492 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 8 Oct 2024 09:09:32 +0800 Subject: [PATCH 12/18] Remove documentation for `notX` relation flags - they'll remain available in API. --- database/relations.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/database/relations.md b/database/relations.md index 6aa912a9..4645104a 100644 --- a/database/relations.md +++ b/database/relations.md @@ -182,7 +182,7 @@ Another key difference between property style and method style relation definiti ```php public function posts(): HasMany { - return $this->hasMany('Acme\Blog\Models\Post')->dependent()->pushable(); + return $this->hasMany('Acme\Blog\Models\Post')->dependent(true)->pushable(); } ``` @@ -190,12 +190,9 @@ The following chained methods are available to define additional parameters abou Method | Description ------ | ----------- -`->dependent()` | Makes this relation "dependent" on the primary model. The related model records will be deleted when the primary model is deleted. This is only available for the following relation types: `attachOne`, `attachMany`, `hasOne`, `hasMany`, `morphOne` and `morphMany`. -`->notDependent()` | Makes this relation independent of the primary model. The related model records will not be deleted when the primary model is deleted. This is the default behavior. -`->detachable()` | Makes this relation detach from the primary model if the primary model is deleted or the relationship is broken. This is the default behavior, and is only available, for the following relation types: `belongsToMany`, `morphToMany` and `morphedByMany`. -`->notDetachable()` | Prevents the relation from detaching from the primary model when the primary model is deleted or the relationship is broken. -`->pushable()` | Sets this relation to save when `push()` is run on the primary model. This is the default behavior. -`->notPushable()` | Sets this relation to not be saved when `push()` is run on the primary model. +`->dependent(true/false)` | Defines if this relation is "dependent" on the primary model. The related model records will be deleted when the primary model is deleted. This is only available for the following relation types: `attachOne`, `attachMany`, `hasOne`, `hasMany`, `morphOne` and `morphMany`. Default: `false`. +`->detachable(true/false)` | Defines if this relation detaches from the primary model if the primary model is deleted or the relationship is broken. This is only available for the following relation types: `belongsToMany`, `morphToMany` and `morphedByMany`. Default: `true`. +`->pushable(true/false)` | Sets this relation to save when `push()` is run on the primary model. Default: `true`. You might have noticed that there are no chain methods for handling "constraint" options like the `order`, `conditions` and `scope` options available in the property style relations. That is because relations defined in this format are already [query builders](query) - you can simply add the constraints directly to the relation! From 3bf9f3177da7ff1297c803816fd7bd02b3adf978 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 8 Oct 2024 09:10:41 +0800 Subject: [PATCH 13/18] Document relation type for relation attribute --- database/relations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/relations.md b/database/relations.md index 4645104a..fe481387 100644 --- a/database/relations.md +++ b/database/relations.md @@ -150,12 +150,12 @@ public function posts(): HasMany } ``` -Alternatively, you may also use the `Relation` attribute on the method to define it as a relation method: +Alternatively, you may also use the `Relation` attribute on the method to define it as a relation method, and include the relation type in the attribute: ```php use Winter\Storm\Database\Attributes\Relation; -#[Relation] +#[Relation('hasMany')] public function posts() { return $this->hasMany('Acme\Blog\Models\Post'); @@ -197,7 +197,7 @@ Method | Description You might have noticed that there are no chain methods for handling "constraint" options like the `order`, `conditions` and `scope` options available in the property style relations. That is because relations defined in this format are already [query builders](query) - you can simply add the constraints directly to the relation! ```php -#[Relation] +#[Relation('hasMany')] public function posts() { return $this->hasMany('Acme\Blog\Models\Post')->dependent()->published()->where('free_article', true); From fcd6438ac84afae3a2170f4e5f9456e142546937 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 8 Oct 2024 09:29:32 +0800 Subject: [PATCH 14/18] Restore note about including IDs in relation queries --- database/relations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database/relations.md b/database/relations.md index fe481387..226f6da0 100644 --- a/database/relations.md +++ b/database/relations.md @@ -1185,6 +1185,8 @@ select * from books select * from authors where id in (1, 2, 3, 4, 5, ...) ``` +> **NOTE:** If you are selecting specific columns in your query and want to load relationships as well, you need to make sure that the columns that contain the keying data (i.e. `id`, `foreign_key`, etc) are included in your select statement. Otherwise, Winter cannot connect the relations. + ### Eager loading multiple relationships Sometimes you may need to eager load several different relationships in a single operation. To do so, just pass additional arguments to the `with` method: From f1d4225c4e639b6dd73e847f50efdf992f87e207 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 8 Oct 2024 10:07:52 +0800 Subject: [PATCH 15/18] Add note about relation conflicts --- database/relations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database/relations.md b/database/relations.md index 226f6da0..3a71dfe7 100644 --- a/database/relations.md +++ b/database/relations.md @@ -175,6 +175,8 @@ $user->profile; // The "Acme\Blog\Models\Profile" record attached to this user $user->posts; // A collection containing every "Acme\Blog\Models\Post" record attached to this user ``` +> **WARNING:** If you define a relation in both the relation properties of a class, and define a method relation with the same name, an exception will be thrown. You must only use one style to define a single relation. You can, however, define multiple unique relations in both the property style and the method style. + ### Detailed relation methods Another key difference between property style and method style relation definitions lies in the additional parameters that may be applied to the relation. With the property style, you can configure the relation by providing additional keys and values in the relation configuration array. With the method style, you define these parameters by using chained methods, for example: From 963d168c21544d348f88cda4484131e41a959040 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 8 Oct 2024 10:20:54 +0800 Subject: [PATCH 16/18] Add note about dynamically defining a relation --- database/relations.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/database/relations.md b/database/relations.md index 3a71dfe7..45a5c02e 100644 --- a/database/relations.md +++ b/database/relations.md @@ -1463,6 +1463,41 @@ $comment->text = 'Edit to this comment!'; $comment->save(); ``` +## Dynamically defining a relation + +Using Winter's powerful [extension capabilities](../services/behaviors), you can dynamically add additional relations to models at run-time, in both the property style and the method style. + +Within a [plugin boot method](../plugin/registration#registration-file), you can extend a given model to add additional relations: + +```php +<?php + +namespace Acme\Blog; + +use Winter\Storm\Database\Relations\HasMany; + +class Plugin extends \System\Classes\PluginBase +{ + // ... + public function boot() + { + \Acme\Blog\Post::extend(function ($model) { + // Property-style + $model->hasMany['comments'] = [ + \Acme\Blog\Comment::class, + ]; + + // Method-style + $model->addDynamicMethod('comments', function (): HasMany { + return $model->hasMany(\Acme\Blog\Comment::class); + }); + }); + } +} +``` + +When using the method style of defining a dynamic relation, you must ensure that the callback function has a return type of one of the applicable relation classes in order for it to be identified as a relation method. + ## Deferred binding Deferred bindings allows you to postpone model relationships binding until the master record commits the changes. This is particularly useful if you need to prepare some models (such as file uploads) and associate them to another model that doesn't exist yet. From f16e7c58e40e3e8b0958c8a0f4b8365e9321549e Mon Sep 17 00:00:00 2001 From: Luke Towers <github@luketowers.ca> Date: Mon, 7 Oct 2024 21:23:41 -0600 Subject: [PATCH 17/18] Update database/relations.md --- database/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/relations.md b/database/relations.md index 45a5c02e..4e580cd8 100644 --- a/database/relations.md +++ b/database/relations.md @@ -116,7 +116,7 @@ public $belongsToMany = [ ### Method style relation definition -> **Note:** Method style relation definition is available from Winter v1.2.5. +> **NOTE:** Method style relation definition is available from Winter v1.2.7. Model relations can also be defined as methods in a model class, similar to the base Laravel framework. From 004ca3a072b197c9e67b4828ffb092126f9f0cf3 Mon Sep 17 00:00:00 2001 From: Ben Thomson <git@alfreido.com> Date: Tue, 8 Oct 2024 13:08:02 +0800 Subject: [PATCH 18/18] Add details on helpers for dynamically defining a relation --- database/relations.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/database/relations.md b/database/relations.md index 45a5c02e..57e95645 100644 --- a/database/relations.md +++ b/database/relations.md @@ -1465,9 +1465,9 @@ $comment->save(); ## Dynamically defining a relation -Using Winter's powerful [extension capabilities](../services/behaviors), you can dynamically add additional relations to models at run-time, in both the property style and the method style. +Using Winter's powerful [extension capabilities](../services/behaviors), you can dynamically add additional relations to models at run-time, in both the property style and the method style, allowing you to extend the functionality of models in other plugins, or in the core of Winter CMS. -Within a [plugin boot method](../plugin/registration#registration-file), you can extend a given model to add additional relations: +Within a [plugin boot method](../plugin/registration#registration-file), you can extend a given model to add additional relations like the following: ```php <?php @@ -1483,13 +1483,13 @@ class Plugin extends \System\Classes\PluginBase { \Acme\Blog\Post::extend(function ($model) { // Property-style - $model->hasMany['comments'] = [ - \Acme\Blog\Comment::class, - ]; + $model->addHasManyRelation(\Acme\Blog\Comment::class, [ + 'delete' => true, + ]); // Method-style $model->addDynamicMethod('comments', function (): HasMany { - return $model->hasMany(\Acme\Blog\Comment::class); + return $model->hasMany(\Acme\Blog\Comment::class)->dependent(); }); }); } @@ -1498,6 +1498,24 @@ class Plugin extends \System\Classes\PluginBase When using the method style of defining a dynamic relation, you must ensure that the callback function has a return type of one of the applicable relation classes in order for it to be identified as a relation method. +Winter provides helper methods to dynamically add relations. The following methods can be used to create relations: + +- `addHasOneRelation()` +- `addHasManyRelation()` +- `addBelongsToRelation()` +- `addBelongsToManyRelation()` +- `addHasOneThroughRelation()` +- `addHasManyThroughRelation()` +- `addAttachOneRelation()` +- `addAttachManyRelation()` +- `addMorphOneRelation()` +- `addMorphManyRelation()` +- `addMorphToRelation()` +- `addMorphToManyRelation()` +- `addMorphedByManyRelation()` + +In all methods above, the first parameter defines the related class, as a class string, and the second parameter provides the relation config as an array. Please note that using these methods results in the relation being defined in the relation properties. + ## Deferred binding Deferred bindings allows you to postpone model relationships binding until the master record commits the changes. This is particularly useful if you need to prepare some models (such as file uploads) and associate them to another model that doesn't exist yet.