@@ -918,20 +918,19 @@ class Model {
918918 * the output of the assigned callable.
919919 * @param bool $from_all_parents When set to `true`, this method will obtain internal objects from all parent
920920 * objects in config. This is only applicable to Models with a `parent_model_class` assigned.
921+ * @param int $limit Pagination limit for internal callables (0 = no limit).
922+ * @param int $offset Pagination offset for internal callables (0 = no offset).
923+ * @param bool $reverse Whether to read in reverse order (oldest first) for internal callables.
921924 * @throws ServerError When neither a `config_path` nor a `internal_callable` are assigned to this model, OR both a
922925 * `config_path` and a `internal_callable` are assigned to this model
923926 * @return array The array of internal objects without any additional processing performed.
924927 */
925- /**
926- * @param bool $from_all_parents Whether to obtain objects from all parent Models.
927- * @param int $limit Pagination hint for internal callables (0 = no limit). When non-zero, callables that accept
928- * a $limit parameter (e.g. log-backed Models) can use it to avoid loading entire datasets
929- * into memory. Callables that do not accept a $limit parameter are called without it.
930- * @param int $offset Pagination hint for internal callables (0 = no offset). When non-zero, callables that accept
931- * an $offset parameter can use it to skip entries. Callables that do not accept an $offset
932- * parameter are called without it.
933- */
934- public function get_internal_objects (bool $ from_all_parents = false , int $ limit = 0 , int $ offset = 0 ): array {
928+ public function get_internal_objects (
929+ bool $ from_all_parents = false ,
930+ int $ limit = 0 ,
931+ int $ offset = 0 ,
932+ bool $ reverse = false ,
933+ ): array {
935934 global $ mock_internal_objects ;
936935
937936 # Throw an error if both `config_path` and `internal_callable` are set.
@@ -957,17 +956,16 @@ class Model {
957956 elseif ($ this ->internal_callable ) {
958957 $ callable = $ this ->internal_callable ;
959958
960- # Forward the pagination limit and offset to callables that accept them. This allows log-backed
961- # Models to stop reading early instead of loading entire log files into memory.
962- # Callables that don't accept these parameters continue to work unchanged.
963- # Reflection results are cached per class+callable to avoid repeated introspection.
964- $ accepts_limit = ($ limit > 0 || $ offset > 0 ) && $ this ->callable_accepts_param ($ callable , 'limit ' );
965- $ accepts_offset = ($ limit > 0 || $ offset > 0 ) && $ this ->callable_accepts_param ($ callable , 'offset ' );
966-
967- if ($ accepts_limit && $ accepts_offset ) {
968- $ internal_objects = $ this ->$ callable (limit: $ limit , offset: $ offset );
969- } elseif ($ accepts_limit ) {
970- $ internal_objects = $ this ->$ callable (limit: $ limit );
959+ # Forward pagination parameters to callables that accept ALL THREE parameters (limit, offset, reverse).
960+ # This allows pagination to be handled by callables that may attempt to load huge amounts of data into
961+ # memory. Callables can use these parameters to selectively load data instead of loading everything.
962+ $ accepts_limit = $ this ->callable_accepts_param ($ callable , 'limit ' );
963+ $ accepts_offset = $ this ->callable_accepts_param ($ callable , 'offset ' );
964+ $ accepts_reverse = $ this ->callable_accepts_param ($ callable , 'reverse ' );
965+ $ needs_pagination = ($ limit > 0 or $ offset > 0 or $ reverse );
966+
967+ if ($ accepts_limit and $ accepts_offset and $ accepts_reverse and $ needs_pagination ) {
968+ $ internal_objects = $ this ->$ callable (limit: $ limit , offset: $ offset , reverse: $ reverse );
971969 } else {
972970 $ internal_objects = $ this ->$ callable ();
973971 }
@@ -2003,11 +2001,11 @@ class Model {
20032001 $ model_name = self ::get_class_fqn ();
20042002 $ model = new $ model_name ();
20052003 $ model_objects = [];
2006- $ requests_pagination = ($ limit or $ offset );
2007- $ cache_exempt = ($ requests_pagination or $ reverse or $ model ->model_cache_exempt );
2004+ $ requests_pagination = ($ limit or $ offset or $ reverse );
2005+ $ cache_exempt = ($ requests_pagination or $ model ->model_cache_exempt );
20082006
20092007 # Throw an error if pagination was requested on a Model without $many enabled
2010- if (!$ model ->many and $ requests_pagination ) {
2008+ if (!$ model ->many and ( $ limit or $ offset ) ) {
20112009 throw new ValidationError (
20122010 message: "Model ` $ model ->verbose_name ` does not support pagination. Please remove the `limit` and/or. " .
20132011 '`offset`parameters and try again. ' ,
@@ -2020,70 +2018,39 @@ class Model {
20202018 return Model::get_model_cache ()::fetch_modelset ($ model_name );
20212019 }
20222020
2023- # Obtain all of this Model's internally stored objects, including those from parent Models if applicable.
2024- # Pass the pagination parameters so that callables (e.g. log readers) can stop early.
2025- # When $reverse is true, the caller wants the oldest entries, so we cannot pre-limit to the newest -
2026- # the full dataset must be loaded to reverse correctly.
2027- #
2028- # For callables that support both limit and offset natively, pass both to allow efficient pagination
2029- # directly at the data source level. For callables that only support limit, we pass limit+offset as
2030- # the limit and then apply the offset via array_slice afterward.
2031- $ pagination_limit = $ limit > 0 && !$ reverse ? $ limit : 0 ;
2032- $ pagination_offset = $ offset > 0 && !$ reverse ? $ offset : 0 ;
2021+ # Check if the callable supports native pagination (all three parameters: limit, offset, reverse)
20332022 $ callable_handled_pagination = false ;
2034-
2035- # Check if the callable supports native offset handling
2036- if ($ model ->internal_callable && $ pagination_offset > 0 ) {
2023+ if ($ model ->internal_callable && $ requests_pagination ) {
20372024 $ callable = $ model ->internal_callable ;
2038- $ callable_handles_offset = $ model ->callable_accepts_param ($ callable , 'offset ' );
2039- $ callable_handles_limit = $ model ->callable_accepts_param ($ callable , 'limit ' );
2040-
2041- if ($ callable_handles_offset && $ callable_handles_limit ) {
2042- # Callable handles both - pagination will be done at source level
2043- $ internal_objects = $ model ->get_internal_objects (
2044- from_all_parents: true ,
2045- limit: $ pagination_limit ,
2046- offset: $ pagination_offset ,
2047- );
2025+ $ callable_handles_all = $ model ->callable_accepts_param ($ callable , 'limit ' ) &&
2026+ $ model ->callable_accepts_param ($ callable , 'offset ' ) &&
2027+ $ model ->callable_accepts_param ($ callable , 'reverse ' );
2028+
2029+ if ($ callable_handles_all ) {
20482030 $ callable_handled_pagination = true ;
2049- } elseif ($ callable_handles_limit ) {
2050- # Callable only handles limit - pass limit+offset and paginate afterward
2051- $ internal_objects = $ model ->get_internal_objects (
2052- from_all_parents: true ,
2053- limit: $ pagination_limit + $ pagination_offset ,
2054- offset: 0 ,
2055- );
2056- } else {
2057- # Callable handles neither - read all and paginate afterward
2058- $ internal_objects = $ model ->get_internal_objects (from_all_parents: true , limit: 0 , offset: 0 );
2059- }
2060- } else {
2061- # No offset or not using internal_callable - use the standard path
2062- $ internal_objects = $ model ->get_internal_objects (
2063- from_all_parents: true ,
2064- limit: $ pagination_limit ,
2065- offset: $ pagination_offset ,
2066- );
2067- # If there's no internal_callable or offset is 0, check if we can skip pagination
2068- if ($ model ->internal_callable && $ pagination_limit > 0 ) {
2069- $ callable = $ model ->internal_callable ;
2070- if (
2071- $ model ->callable_accepts_param ($ callable , 'limit ' ) &&
2072- $ model ->callable_accepts_param ($ callable , 'offset ' )
2073- ) {
2074- $ callable_handled_pagination = true ;
2075- }
20762031 }
20772032 }
20782033
2034+ # Obtain all internal objects, passing pagination params for callables that support them
2035+ $ internal_objects = $ model ->get_internal_objects (
2036+ from_all_parents: true ,
2037+ limit: $ limit ,
2038+ offset: $ offset ,
2039+ reverse: $ reverse ,
2040+ );
2041+
20792042 # For non `many` Models, wrap the internal object in an array so we can loop
20802043 $ internal_objects = $ model ->many ? $ internal_objects : [$ internal_objects ];
20812044
2082- # Reverse the order we read the objects and/or paginate if requested
2083- # Skip pagination if the callable already handled it natively
2084- $ internal_objects = $ reverse ? array_reverse ($ internal_objects , preserve_keys: true ) : $ internal_objects ;
2085- if (!$ callable_handled_pagination ) {
2086- $ internal_objects = self ::paginate ($ internal_objects , $ limit , $ offset , preserve_keys: true );
2045+ # Apply pagination and/or reverse if the callable did not handle it natively
2046+ if (!$ callable_handled_pagination && $ requests_pagination ) {
2047+ if ($ reverse ) {
2048+ # Paginate from the beginning (oldest entries), then reverse for display
2049+ $ internal_objects = array_reverse ($ internal_objects , preserve_keys: true );
2050+ $ internal_objects = self ::paginate ($ internal_objects , $ limit , $ offset , preserve_keys: true );
2051+ } else {
2052+ $ internal_objects = self ::paginate ($ internal_objects , $ limit , $ offset , preserve_keys: true );
2053+ }
20872054 }
20882055
20892056 # Loop through each internal object and create a Model object for it
@@ -2095,8 +2062,12 @@ class Model {
20952062 $ parent_id = $ parent_model ? $ parent_model ->id : null ;
20962063 $ internal_object = $ parent_model ? $ internal_object ['_internal_object ' ] : $ internal_object ;
20972064
2098- # Normalize IDs
2065+ # Normalize IDs - when the callable handled pagination natively, the array indices start
2066+ # at 0 but the actual IDs should account for the offset
20992067 $ internal_id = is_numeric ($ internal_id ) ? (int ) $ internal_id : $ internal_id ;
2068+ if ($ callable_handled_pagination && is_int ($ internal_id )) {
2069+ $ internal_id += $ offset ;
2070+ }
21002071
21012072 # Create a new Model object for this internal object and assign its ID
21022073 $ model_object = new $ model (id: $ internal_id , parent_id: $ parent_id , skip_init: true );
0 commit comments