22
33use Exception ;
44
5- use Winter \Sorm \Database \Model ;
5+ use Winter \Storm \Database \Model ;
6+ use Winter \Storm \Database \Models \DeferredBinding ;
67
78/**
89 * HasSortableRelations trait
1718 *
1819 * To set orders:
1920 *
20- * $model->setSortableRelationOrder ($relationName, $recordIds, $recordOrders);
21+ * $model->setRelationOrder ($relationName, $recordIds, $recordOrders);
2122 *
2223 */
2324trait HasSortableRelations
@@ -41,8 +42,20 @@ public function initializeHasSortableRelations() : void
4142 if (array_key_exists ($ relationName , $ sortableRelations )) {
4243 $ column = $ this ->getRelationSortOrderColumn ($ relationName );
4344
45+ // If the records were attached with an explicit sort order (e.g. when committing
46+ // deferred bindings that already carry a pivot sort order), keep it - otherwise
47+ // auto-append the records to the end of the relation. $data is the list of pivot
48+ // insert rows, one per attached record.
49+ if (is_array ($ data )) {
50+ foreach ($ data as $ row ) {
51+ if (is_array ($ row ) && isset ($ row [$ column ])) {
52+ return ;
53+ }
54+ }
55+ }
56+
4457 foreach ($ attached as $ id ) {
45- $ this ->updateRelationOrder ($ relationName , $ id , $ column );
58+ $ this ->updateRelationOrder ($ relationName , $ id , $ column, null );
4659 }
4760 }
4861 });
@@ -52,39 +65,62 @@ public function initializeHasSortableRelations() : void
5265 if (array_key_exists ($ relationName , $ sortableRelations )) {
5366 $ column = $ this ->getRelationSortOrderColumn ($ relationName );
5467
55- $ this ->updateRelationOrder ($ relationName , $ relatedModel ->getKey (), $ column );
68+ // No order - auto-append to the end of the relation.
69+ $ this ->updateRelationOrder ($ relationName , $ relatedModel ->getKey (), $ column , null );
5670 }
5771 });
5872
5973 foreach ($ sortableRelations as $ relationName => $ column ) {
6074 $ relationType = $ this ->getRelationType ($ relationName );
61- if (!in_array ($ relationType , ['belongsToMany ' , 'morphToMany ' ])) {
75+ if (!in_array ($ relationType , ['belongsToMany ' , 'morphToMany ' , ' morphedByMany ' ])) {
6276 continue ;
6377 }
6478 $ definition = $ this ->getRelationDefinition ($ relationName );
65- $ pivot = array_wrap (array_get ($ definition , 'pivot ' , []));
6679
80+ // Make sure the sort order column is available as pivot data.
81+ $ pivot = array_wrap (array_get ($ definition , 'pivot ' , []));
6782 if (!in_array ($ column , $ pivot )) {
68- // Make sure the sort order column is available as pivot data.
6983 $ pivot [] = $ column ;
7084 $ definition ['pivot ' ] = $ pivot ;
71- $ this ->$ relationType [$ relationName ] = $ definition ;
7285 }
86+
87+ // Auto-add an ordering clause so the relation is returned in sort order by default.
88+ // Qualify with the pivot table name to avoid colliding with a column of the same
89+ // name on the related model (which would silently order by the wrong column).
90+ if (!array_key_exists ('order ' , $ definition )) {
91+ $ pivotTable = array_get ($ definition , 'table ' );
92+ $ definition ['order ' ] = ($ pivotTable ? $ pivotTable . '. ' : '' ) . $ column . ' asc ' ;
93+ }
94+
95+ $ this ->$ relationType [$ relationName ] = $ definition ;
7396 }
7497 }
7598
7699 /**
77- * Set the sort order of records to the specified orders. If the orders is
78- * undefined, the record identifier is used.
100+ * Set the sort order of records to the specified orders. If the orders are
101+ * undefined, a sequential 1..N order is assigned in the given id order.
102+ *
103+ * When a $sessionKey is provided the relation is operating in deferred mode:
104+ * the sort order is written to the pivot_data of the matching deferred_bindings
105+ * records instead of the pivot table. The caller (e.g. RelationController) is
106+ * responsible for passing the sessionKey only when in deferred mode.
79107 */
80- public function setRelationOrder (string $ relationName , string |int |array $ itemIds , array $ itemOrders = []) : void
81- {
108+ public function setRelationOrder (
109+ string $ relationName ,
110+ string |int |array $ itemIds ,
111+ array $ itemOrders = [],
112+ ?string $ sessionKey = null
113+ ) : void {
82114 if (!is_array ($ itemIds )) {
83115 $ itemIds = [$ itemIds ];
84116 }
85117
118+ if (empty ($ itemIds )) {
119+ return ;
120+ }
121+
86122 if (empty ($ itemOrders )) {
87- $ itemOrders = $ itemIds ;
123+ $ itemOrders = range ( 1 , count ( $ itemIds)) ;
88124 }
89125
90126 if (count ($ itemIds ) != count ($ itemOrders )) {
@@ -93,21 +129,56 @@ public function setRelationOrder(string $relationName, string|int|array $itemIds
93129
94130 $ column = $ this ->getRelationSortOrderColumn ($ relationName );
95131
132+ /*
133+ * Deferred mode - update pivot_data on the deferred_bindings records.
134+ * Batch the lookup into a single query, then save each binding (each row
135+ * carries its own JSON pivot_data so individual saves are required).
136+ */
137+ if ($ sessionKey ) {
138+ $ bindings = DeferredBinding::where ('master_type ' , get_class ($ this ))
139+ ->where ('master_field ' , $ relationName )
140+ ->whereIn ('slave_id ' , $ itemIds )
141+ ->where ('session_key ' , $ sessionKey )
142+ ->where ('is_bind ' , 1 )
143+ ->get ()
144+ ->keyBy ('slave_id ' );
145+
146+ foreach ($ itemIds as $ index => $ id ) {
147+ if ($ binding = $ bindings ->get ($ id )) {
148+ $ pivotData = $ binding ->pivot_data ?: [];
149+ $ pivotData [$ column ] = (int ) $ itemOrders [$ index ];
150+ $ binding ->pivot_data = $ pivotData ;
151+ $ binding ->save ();
152+ }
153+ }
154+
155+ return ;
156+ }
157+
96158 foreach ($ itemIds as $ index => $ id ) {
97- $ order = (int )$ itemOrders [$ index ];
98- $ this ->updateRelationOrder ($ relationName , $ id , $ column , $ order );
159+ // Pass the explicit order (which may legitimately be 0) so it is not mistaken
160+ // for the "auto-append" case.
161+ $ this ->updateRelationOrder ($ relationName , $ id , $ column , (int ) $ itemOrders [$ index ]);
99162 }
100163 }
101164
102165 /**
103- * Update relation record sort_order.
166+ * Update relation record sort_order. A null $order auto-appends the record to the end
167+ * of the relation; an explicit integer (including 0) is written as-is.
104168 */
105- protected function updateRelationOrder (string $ relationName , string |int |Model $ id , string $ column , int $ order = 0 ) : void
169+ protected function updateRelationOrder (string $ relationName , string |int |Model $ id , string $ column , ? int $ order = null ) : void
106170 {
107171 $ relation = $ this ->{$ relationName }();
108172
109- if (!$ order ) {
110- $ order = $ relation ->count ();
173+ if ($ order === null ) {
174+ // Append to the end of the relation. For pivot-based relations use the pivot
175+ // query directly so a defined "order" clause on the relation cannot interfere
176+ // with the aggregate, and so non-contiguous sort orders are handled correctly.
177+ if (method_exists ($ relation , 'newPivotQuery ' )) {
178+ $ order = ((int ) $ relation ->newPivotQuery ()->max ($ column )) + 1 ;
179+ } else {
180+ $ order = $ relation ->count () + 1 ;
181+ }
111182 }
112183 if (method_exists ($ relation , 'updateExistingPivot ' )) {
113184 $ relation ->updateExistingPivot ($ id , [ $ column => (int )$ order ]);
@@ -123,7 +194,15 @@ protected function updateRelationOrder(string $relationName, string|int|Model $i
123194 }
124195
125196 /**
126- * Get the name of the "sort_order" column.
197+ * Returns true if the given relation is configured as a sortable relation.
198+ */
199+ public function isSortableRelation (string $ relationName ) : bool
200+ {
201+ return array_key_exists ($ relationName , $ this ->getSortableRelations ());
202+ }
203+
204+ /**
205+ * Get the name of the "sort_order" column for the given relation.
127206 */
128207 public function getRelationSortOrderColumn (string $ relationName ) : string
129208 {
@@ -133,7 +212,7 @@ public function getRelationSortOrderColumn(string $relationName) : string
133212 /**
134213 * Return all configured sortable relations.
135214 */
136- protected function getSortableRelations () : array
215+ public function getSortableRelations () : array
137216 {
138217 if (property_exists ($ this , 'sortableRelations ' )) {
139218 return $ this ->sortableRelations ;
0 commit comments