44use Db ;
55use Site ;
66use Exception ;
7+ use ReflectionMethod ;
78
89/**
910 * Translatable trait provides per-row model translation using a single
@@ -47,17 +48,11 @@ trait Translatable
4748 protected $ translatableBaseValues = [];
4849
4950 /**
50- * @var array|null translatableBatch holds translations preloaded for the hydration
51- * batch on this hydration prototype , keyed by model_id then attribute
51+ * @var array|null translatableBatch holds the locale and translations preloaded
52+ * while this model hydrates a batch of rows , keyed by model key then attribute
5253 */
5354 protected $ translatableBatch ;
5455
55- /**
56- * @var array|null translatableHydrationData supplies raw translations only while
57- * this instance runs its fetched lifecycle, without bypassing its loader
58- */
59- protected $ translatableHydrationData ;
60-
6156 /**
6257 * initializeTranslatable trait for a model
6358 */
@@ -693,35 +688,24 @@ public function getTranslatableOriginals($locale = null)
693688 //
694689
695690 /**
696- * hydrateWithTranslatableBatch loads the active locale translations for all rows
697- * in one query before hydration , so each fetched event can promote translated values
698- * without its own lookup. Rows or locales outside the batch fall back to a lookup .
691+ * hydrateWithTranslatableBatch preloads the active locale translations for the rows
692+ * in chunked queries , so fetched events can promote translated values without a
693+ * query per row. Models with their own loader or table getter load per row .
699694 * @internal Called by the database builder.
700695 */
701696 public function hydrateWithTranslatableBatch (array $ items , callable $ hydrate )
702697 {
703- $ keyName = $ this ->getKeyName ();
704- $ ids = array_column ($ items , $ keyName );
698+ $ ids = array_column ($ items , $ this ->getKeyName ());
705699
706- if (!$ ids || !$ this ->shouldTranslate ()) {
700+ if (!$ ids || !$ this ->shouldTranslate () || ! $ this -> usesDefaultTranslatableStorage () ) {
707701 return $ hydrate ();
708702 }
709703
710- // Custom loaders or table getters may depend on hydrated attributes or
711- // external storage. Only preload this trait's default storage behavior.
712- foreach (['loadTranslatableData ' , 'getTranslateAttributeTable ' ] as $ method ) {
713- $ implementation = new \ReflectionMethod ($ this , $ method );
714- $ default = new \ReflectionMethod (__TRAIT__ , $ method );
715- if ($ implementation ->getFileName () !== __FILE__ || $ implementation ->getStartLine () !== $ default ->getStartLine ()) {
716- return $ hydrate ();
717- }
718- }
719-
720704 $ locale = $ this ->getTranslatableContext ();
721- $ connection = Db:: connection ( );
722- $ rows = [];
705+ $ rows = array_fill_keys ( $ ids , [] );
706+
723707 foreach (array_chunk ($ ids , 500 ) as $ chunk ) {
724- $ translations = $ connection -> table ($ this ->getTranslateAttributeTable ())
708+ $ translations = Db:: table ($ this ->getTranslateAttributeTable ())
725709 ->where ('model_type ' , $ this ->getMorphClass ())
726710 ->whereIn ('model_id ' , $ chunk )
727711 ->where ('locale ' , $ locale )
@@ -732,69 +716,57 @@ public function hydrateWithTranslatableBatch(array $items, callable $hydrate)
732716 }
733717 }
734718
735- $ previous = $ this ->translatableBatch ;
736719 $ this ->translatableBatch = [
737- 'connection ' => $ connection ,
738- 'table ' => $ this ->getTranslateAttributeTable (),
739- 'morph ' => $ this ->getMorphClass (),
740720 'locale ' => $ locale ,
741- 'ids ' => array_fill_keys ( $ ids , true ),
721+ 'morph ' => $ this -> getMorphClass ( ),
742722 'rows ' => $ rows ,
743723 ];
744724
745725 try {
746726 return $ hydrate ();
747727 }
748728 finally {
749- $ this ->translatableBatch = $ previous ;
729+ $ this ->translatableBatch = null ;
750730 }
751731 }
752732
753733 /**
754- * newFromBuilderWithTranslatableBatch keeps the snapshot local to one model
755- * while lifecycle callbacks may independently hydrate other models .
756- * @internal Called by the database model during hydration .
734+ * applyTranslatableBatch gives a model hydrated from this one the translations
735+ * preloaded for its row, as the default loader would have read them .
736+ * @internal Called by the database model before the fetched event .
757737 */
758- public function newFromBuilderWithTranslatableBatch ( $ attributes , $ connection )
738+ public function applyTranslatableBatch ( $ model )
759739 {
760- $ batch = $ this ->translatableBatch ;
761- if ($ batch === null ) {
762- return $ this ->newFromBuilderInstance ($ attributes , $ connection );
763- }
740+ $ rows = $ this ->translatableBatch ['rows ' ] ?? null ;
741+ $ key = $ model ->getKey ();
764742
765- $ this ->translatableBatch = null ;
766- $ hydrated = null ;
767- try {
768- return $ this ->newFromBuilderInstance ($ attributes , $ connection , function ($ instance ) use ($ batch , &$ hydrated ) {
769- $ hydrated = $ instance ;
770- $ instance ->translatableHydrationData = $ batch ;
771- });
743+ if ($ rows === null || $ key === null || !array_key_exists ($ key , $ rows )) {
744+ return ;
772745 }
773- finally {
774- if ($ hydrated !== null ) {
775- $ hydrated ->translatableHydrationData = null ;
776- }
777- $ this ->translatableBatch = $ batch ;
746+
747+ // A morph class that depends on the row is stored under another type
748+ if ($ model ->getMorphClass () !== $ this ->translatableBatch ['morph ' ]) {
749+ return ;
778750 }
751+
752+ $ locale = $ this ->translatableBatch ['locale ' ];
753+ $ model ->translatableAttributes [$ locale ] = $ rows [$ key ];
754+ $ model ->translatableOriginals [$ locale ] = $ rows [$ key ];
779755 }
780756
781757 /**
782- * getTranslatableBatchRows returns preloaded translations for this model, or null
783- * when the model is not part of the batch in progress .
758+ * usesDefaultTranslatableStorage checks that the model keeps this trait's loader
759+ * and table getter, since overrides may read other storage or row attributes .
784760 */
785- protected function getTranslatableBatchRows ( $ locale , array $ batch )
761+ protected function usesDefaultTranslatableStorage (): bool
786762 {
787- if (
788- $ batch ['connection ' ] !== Db::connection () ||
789- $ batch ['locale ' ] !== $ locale ||
790- $ batch ['morph ' ] !== $ this ->getMorphClass () ||
791- $ batch ['table ' ] !== $ this ->getTranslateAttributeTable () ||
792- !isset ($ batch ['ids ' ][$ this ->getKey ()])
793- ) {
794- return null ;
763+ foreach (['loadTranslatableData ' , 'getTranslateAttributeTable ' ] as $ method ) {
764+ if ((new ReflectionMethod ($ this , $ method ))->getFileName () !== __FILE__ ) {
765+ return false ;
766+ }
795767 }
796768
797- return $ batch [ ' rows ' ][ $ this -> getKey ()] ?? [] ;
769+ return true ;
798770 }
799771
800772 /**
@@ -893,12 +865,7 @@ protected function loadTranslatableData($locale)
893865 $ rows = [];
894866 }
895867 else {
896- // Model overrides still run through this loader when rows are preloaded.
897- $ rows = $ this ->translatableHydrationData !== null
898- ? $ this ->getTranslatableBatchRows ($ locale , $ this ->translatableHydrationData )
899- : null ;
900-
901- $ rows ??= Db::table ($ this ->getTranslateAttributeTable ())
868+ $ rows = Db::table ($ this ->getTranslateAttributeTable ())
902869 ->where ('model_type ' , $ this ->getMorphClass ())
903870 ->where ('model_id ' , $ this ->getKey ())
904871 ->where ('locale ' , $ locale )
0 commit comments