Skip to content

Commit c33f888

Browse files
refactor: prevent child model caching from clobbering from internal loads
1 parent 3cf3733 commit c33f888

File tree

5 files changed

+288
-56
lines changed

5 files changed

+288
-56
lines changed

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/BaseTraits.inc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ trait BaseTraits {
1414
* Obtains the shortname of the called class.
1515
* @return string The shortname for this object's class.
1616
*/
17-
public function get_class_shortname(): string {
18-
return (new ReflectionClass($this))->getShortName();
17+
public static function get_class_shortname(): string {
18+
return (new ReflectionClass(static::class))->getShortName();
1919
}
2020

2121
/**
2222
* Obtains the fully qualified name of the called class.
2323
* @return string The FQN for this object's class.
2424
*/
25-
public function get_class_fqn(): string {
26-
return (new ReflectionClass($this))->getName();
25+
public static function get_class_fqn(): string {
26+
return (new ReflectionClass(static::class))->getName();
2727
}
2828

2929
/**

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Model.inc

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,6 @@ class Model {
144144
*/
145145
public Cache|null $cache = null;
146146

147-
/**
148-
* @var array $object_cache
149-
* An array used internally to cache already loaded ModelSets for any Model class. This prevents redundant loading
150-
* of ModelSet objects during operations that may require multiple references to the same ModelSet.
151-
*/
152-
public static array $object_cache = [];
153-
154147
/**
155148
* @var ModelSet $related_objects
156149
* A ModelSet containing foreign Model objects related to this Model. These are primarily populated by
@@ -375,7 +368,7 @@ class Model {
375368
*/
376369
public function __sleep() {
377370
# Variables
378-
$excluded_properties = ['initial_object', 'client', 'parent_model', 'related_objects', 'object_cache'];
371+
$excluded_properties = ['initial_object', 'client', 'parent_model', 'related_objects'];
379372
$properties = array_keys(get_object_vars($this));
380373

381374
# Filter out excluded properties from the list of properties to serialize
@@ -409,7 +402,7 @@ class Model {
409402
*/
410403
private function set_construct_options(array $options): array {
411404
# Set the async flag if given. Ensure this defaults to true.
412-
$this->async = isset($options['async']) ? $options['async'] : true;
405+
$this->async = $options['async'] ?? true;
413406
unset($options['async']);
414407

415408
# Set the placement index if given and is a positive integer
@@ -529,12 +522,20 @@ class Model {
529522
return new $class();
530523
}
531524

525+
/**
526+
* Provides access to the ModelCache singleton instance.
527+
* @return ModelCache The ModelCache singleton instance.
528+
*/
529+
public static function get_model_cache(): ModelCache {
530+
return ModelCache::get_instance();
531+
}
532+
532533
/**
533534
* Clears the object cache for this Model class.
534535
*/
535-
public static function clear_object_cache(): void {
536+
public static function clear_model_cache(): void {
536537
# Clear the object cache
537-
self::$object_cache = [];
538+
self::get_model_cache()::clear();
538539
}
539540

540541
/**
@@ -655,7 +656,7 @@ class Model {
655656
*/
656657
public static function set_config(string $path, mixed $value, mixed $default = null): mixed {
657658
# Clear the object cache for this Model class since config is being modified
658-
self::clear_object_cache();
659+
self::clear_model_cache();
659660
return config_set_path(self::normalize_config_path($path), $value, $default);
660661
}
661662

@@ -701,7 +702,7 @@ class Model {
701702
*/
702703
public static function del_config(string $path): mixed {
703704
# Clear the object cache for this Model class since config is being modified
704-
self::clear_object_cache();
705+
self::clear_model_cache();
705706
return config_del_path(self::normalize_config_path($path));
706707
}
707708

@@ -739,7 +740,7 @@ class Model {
739740
session_destroy();
740741

741742
# Clear the object cache for this Model class since config has been modified
742-
self::clear_object_cache();
743+
self::clear_model_cache();
743744

744745
# If a subsystem is specified for this Model, mark it as dirty
745746
if ($this->subsystem) {
@@ -770,7 +771,7 @@ class Model {
770771
global $config;
771772

772773
# Clear the object cache for all Model classes since config is being reloaded
773-
self::clear_object_cache();
774+
self::clear_model_cache();
774775

775776
$config = parse_config(parse: $force_parse);
776777
}
@@ -867,27 +868,34 @@ class Model {
867868
/**
868869
* Recursively obtains all internal objects for this Model from all parent objects in config. This method is only
869870
* applicable to Models with a `parent_model_class` assigned.
870-
* @return array An array of all internal objects for this Model from all parent objects in
871+
* @return array An array containing the internal object and its context information needed to reconstruct the
872+
* Model object later. This array will always be structured as the following:
873+
* [
874+
* "_parent_model" => Model, // The parent Model object
875+
* "_id" => string|int, // The ID of the internal object
876+
* "_internal_object" => array, // The internal object data
877+
* ]
871878
*/
872879
protected function get_internal_objects_from_all_parents(): array {
873-
# Variables
874-
$internal_objects = [];
880+
/**@var Model $parent_model_class The parent Model class assigned to this Model. */
875881
$parent_model_class = "\\RESTAPI\\Models\\$this->parent_model_class";
876-
$parent_model = new $parent_model_class(skip_init: true);
877-
$parent_configs = $this->get_config($parent_model->config_path, []);
882+
$parent_modelset = $parent_model_class::read_all();
883+
$internal_objects = [];
878884

879885
# Check for internal objects from all parents
880-
foreach ($parent_configs as $parent_id => $parent_config) {
881-
# Obtain the list of child objects from this parent config
882-
$child_configs = $this->get_config("$parent_model->config_path/$parent_id/$this->config_path", []);
883-
$parent_model = new $parent_model_class(id: $parent_id, skip_init: true);
884-
$parent_model->from_internal_object($parent_config);
886+
foreach ($parent_modelset->model_objects as $parent_model) {
887+
# Obtain the internal config for
888+
$child_configs = $this->get_config(
889+
"$parent_model->config_path/$parent_model->id/$this->config_path", []
890+
);
885891

886892
foreach ($child_configs as $child_id => $child_config) {
887-
$child_config['parent_model'] = $parent_model;
888-
$child_config['parent_id'] = $parent_id;
889-
$child_config['id'] = $child_id;
890-
$internal_objects[] = $child_config;
893+
# Retain internal object context information needed to reconstruct the Model object later
894+
$internal_objects[] = [
895+
"_parent_model" => $parent_model,
896+
"_id" => $child_id,
897+
"_internal_object" => $child_config,
898+
];
891899
}
892900
}
893901

@@ -898,11 +906,13 @@ class Model {
898906
* Obtains all internal objects for this Model. When a `config_path` is specified, this method will obtain the
899907
* internal objects directly from config. When an `internal_callable` is assigned, this method will return
900908
* the output of the assigned callable.
909+
* @param bool $from_all_parents When set to `true`, this method will obtain internal objects from all parent
910+
* objects in config. This is only applicable to Models with a `parent_model_class` assigned.
901911
* @throws ServerError When neither a `config_path` nor a `internal_callable` are assigned to this model, OR both a
902912
* `config_path` and a `internal_callable` are assigned to this model
903913
* @return array The array of internal objects without any additional processing performed.
904914
*/
905-
public function get_internal_objects(): array {
915+
public function get_internal_objects(bool $from_all_parents = false): array {
906916
global $mock_internal_objects;
907917

908918
# Throw an error if both `config_path` and `internal_callable` are set.
@@ -916,8 +926,8 @@ class Model {
916926
elseif ($mock_internal_objects) {
917927
$internal_objects = $mock_internal_objects;
918928
}
919-
# Obtain all internal objects from all parents if a parent Model class is assigned
920-
elseif ($this->parent_model_class) {
929+
# Obtain all internal objects from all parents if a parent Model class is assigned and all parents is requested
930+
elseif ($from_all_parents and $this->parent_model_class) {
921931
$internal_objects = $this->get_internal_objects_from_all_parents();
922932
}
923933
# Obtain the internal objects from the config path if specified
@@ -977,8 +987,10 @@ class Model {
977987

978988
# When `many` is enabled, obtain the object with our ID. Otherwise, just assign the direct object
979989
$internal_object = $this->many ? $internal_objects[$this->id] : $internal_objects;
980-
981990
$this->from_internal_object($internal_object);
991+
992+
# Re-index the ModelCache for this object since it has been refreshed from internal
993+
self::get_model_cache()::cache_model($this);
982994
}
983995

984996
/**
@@ -1932,7 +1944,7 @@ class Model {
19321944
*/
19331945
public static function read_all(int $limit = 0, int $offset = 0, bool $reverse = false): ModelSet|Model {
19341946
# Variables
1935-
$model_name = get_called_class();
1947+
$model_name = self::get_class_fqn();
19361948
$model = new $model_name();
19371949
$model_objects = [];
19381950
$requests_pagination = ($limit or $offset);
@@ -1948,12 +1960,12 @@ class Model {
19481960
}
19491961

19501962
# Load from cache if it is not exempt and cached objects exist
1951-
if (!$cache_exempt and Model::$object_cache[$model_name]) {
1952-
return Model::$object_cache[$model_name];
1963+
if (!$cache_exempt and self::get_model_cache()::has_modelset($model_name)) {
1964+
return Model::get_model_cache()::fetch_modelset($model_name);
19531965
}
19541966

1955-
# Obtain all of this Model's internally stored objects
1956-
$internal_objects = $model->get_internal_objects();
1967+
# Obtain all of this Model's internally stored objects, including those from parent Models if applicable
1968+
$internal_objects = $model->get_internal_objects(from_all_parents: true);
19571969

19581970
# For non `many` Models, wrap the internal object in an array so we can loop
19591971
$internal_objects = $model->many ? $internal_objects : [$internal_objects];
@@ -1964,15 +1976,19 @@ class Model {
19641976

19651977
# Loop through each internal object and create a Model object for it
19661978
foreach ($internal_objects as $internal_id => $internal_object) {
1967-
# Populate the ID and parent ID values where applicable
1968-
$internal_id = array_key_exists('id', $internal_object) ? $internal_object['id'] : $internal_id;
1979+
# For Models with parent Models, the internal object always contains extra context data about
1980+
# the parent Model class. See the get_internal_objects_from_all_parents() method for more information.
1981+
$internal_id = $model->parent_model_class ? $internal_object["_id"] : $internal_id;
1982+
$parent_model = $model->parent_model_class ? $internal_object["_parent_model"] : null;
1983+
$parent_id = $parent_model ? $parent_model->id : null;
1984+
$internal_object = $parent_model ? $internal_object["_internal_object"] : $internal_object;
1985+
1986+
# Normalize IDs
19691987
$internal_id = is_numeric($internal_id) ? (int) $internal_id : $internal_id;
1970-
$parent_id = array_key_exists('parent_id', $internal_object) ? $internal_object['parent_id'] : null;
1971-
$parent_id = is_numeric($parent_id) ? (int) $parent_id : $parent_id;
19721988

19731989
# Create a new Model object for this internal object and assign its ID
19741990
$model_object = new $model(id: $internal_id, parent_id: $parent_id, skip_init: true);
1975-
$model_object->parent_model = $internal_object['parent_model'] ?? null;
1991+
$model_object->parent_model = $parent_model;
19761992

19771993
# Populate the Model object using its current internal values and add it to the array of all Model objects
19781994
$model_object->from_internal_object($internal_object);
@@ -1985,7 +2001,7 @@ class Model {
19852001

19862002
# For many models, cache the ModelSet if not exempt
19872003
if ($model->many and !$cache_exempt) {
1988-
Model::$object_cache[$model_name] = new ModelSet(model_objects: $model_objects);
2004+
self::get_model_cache()::cache_modelset($model_name, $modelset);
19892005
}
19902006

19912007
# For many enabled Models return a ModelSet, otherwise return a single Model object
@@ -2088,6 +2104,9 @@ class Model {
20882104
# Obtain the requested Model object from config by ID
20892105
$this->from_internal();
20902106

2107+
# Cache the newly read Model object
2108+
self::get_model_cache()::cache_model($this);
2109+
20912110
return $this;
20922111
}
20932112

@@ -2149,7 +2168,7 @@ class Model {
21492168
}
21502169

21512170
# Reset the object cache as the config has changed
2152-
Model::$object_cache = [];
2171+
self::clear_model_cache();
21532172

21542173
# Return the current representation of this object
21552174
return $this;
@@ -2242,7 +2261,7 @@ class Model {
22422261
}
22432262

22442263
# Reset the object cache as the config has changed
2245-
Model::$object_cache = [];
2264+
self::clear_model_cache();
22462265

22472266
# Return the current representation of this object
22482267
return $this;
@@ -2338,7 +2357,7 @@ class Model {
23382357
}
23392358

23402359
# Reset the object cache as the config has changed
2341-
Model::$object_cache = [];
2360+
self::clear_model_cache();
23422361

23432362
return $new_objects;
23442363
}
@@ -2426,7 +2445,7 @@ class Model {
24262445
}
24272446

24282447
# Reset the object cache as the config has changed
2429-
Model::$object_cache = [];
2448+
self::clear_model_cache();
24302449

24312450
# Return the current representation of this object
24322451
return $this;
@@ -2470,7 +2489,7 @@ class Model {
24702489
$model_objects = self::query(query_params: $query_params, excluded: $excluded, limit: $limit, offset: $offset);
24712490

24722491
# Reset the object cache as the config has changed
2473-
Model::$object_cache = [];
2492+
Model::clear_model_cache();
24742493

24752494
# Delete the Model objects that matched the query
24762495
return $model_objects->delete();
@@ -2484,7 +2503,7 @@ class Model {
24842503
$model_objects = self::read_all();
24852504

24862505
# Reset the object cache as the config has changed
2487-
Model::$object_cache = [];
2506+
Model::clear_model_cache();
24882507

24892508
# Delete all Model objects for this Model
24902509
return $model_objects->delete();

0 commit comments

Comments
 (0)