Skip to content

Commit f46a205

Browse files
refactor: remove model indexing
Model indexing isn't practical here because the cache isn't retained between requests. It makes more sense to cache logical queries instead of basic indexing.
1 parent e2effed commit f46a205

File tree

3 files changed

+13
-260
lines changed

3 files changed

+13
-260
lines changed

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

Lines changed: 5 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,11 @@ class ModelCache {
2121
public static ?self $instance = null;
2222

2323
/**
24-
* @var array $index An associative array that contains cache Model objects that have been loaded and
25-
* indexed by specific fields for quick lookup. Structured as:
26-
*
27-
* [Model class name] => [
28-
* [index field name] => [
29-
* [index field value] => Model object
30-
* ]
31-
* ]
32-
*/
33-
public static array $index = [];
34-
35-
/**
36-
* @var array $cache An associative array that contains cached Model objects that have been loaded into memory as
37-
* a complete ModelSet of every object of that Model type. Structured as: [Model class name] => ModelSet object
24+
* @var array $cache An associative array that contains cached ModelSet objects that have been loaded into memory.
25+
* This includes root ModelSets that contain all Model objects of a given Model class as well as indexed queries
26+
* that have already been run against those ModelSets. Structured as: [ModelSet signature => ModelSet object].
27+
* Please note that root ModelSets are indexed by their qualified Model class name only, whereas queried ModelSets
28+
* are indexed by the root signature followed by a series of query hashes separated by colons.
3829
*/
3930
public static array $cache = [];
4031

@@ -83,143 +74,10 @@ class ModelCache {
8374
return self::$cache[$model_class];
8475
}
8576

86-
/**
87-
* Raises an error if the given Model object does not support being indexed by the specified field.
88-
* @param Model $model The Model object to check for uniqueness.
89-
* @param string $index_field The index field name to check for uniqueness.
90-
* @throws ServerError If the Model is attempting to be indexed by a non-unique field.
91-
* @throws ServerError If the Model is not many-enabled.
92-
*/
93-
private static function ensure_model_supports_indexing(Model $model, string $index_field): void {
94-
# Only many-enabled Models can be indexed
95-
if (!$model->many) {
96-
throw new ServerError(
97-
message: "Cannot index Model class '" . $model->get_class_fqn() . 'because it is not many-enabled.',
98-
response_id: 'MODEL_CACHE_INDEX_FIELD_ON_NON_MANY_MODEL',
99-
);
100-
}
101-
102-
# Models with parent model classes cannot be indexed
103-
if ($model->parent_model_class) {
104-
throw new ServerError(
105-
message: "Cannot index Model class '" .
106-
$model->get_class_fqn() .
107-
"' because it has a parent model class '" .
108-
$model->parent_model_class .
109-
"'.",
110-
response_id: 'MODEL_CACHE_INDEX_FIELD_ON_PARENTED_MODEL',
111-
);
112-
}
113-
114-
# If indexing by 'id', it's always unique
115-
if ($index_field === 'id') {
116-
return;
117-
}
118-
119-
# Check if the index field is unique on the Model object
120-
if (!$model->$index_field->unique) {
121-
throw new ServerError(
122-
message: "Cannot index Model class '" .
123-
$model->get_class_fqn() .
124-
"' by non-unique field " .
125-
"'$index_field'.",
126-
response_id: 'MODEL_CACHE_INDEX_FIELD_NOT_UNIQUE',
127-
);
128-
}
129-
}
130-
131-
/**
132-
* Indexes the ModelSet cache for a given Model class by a specified field. This method will populate
133-
* the $index array for the specified Model class and index field.
134-
* @param string $model_class The Model class name whose ModelSet is to be indexed. If no cached ModelSet exists,
135-
* one will be created by reading all Model objects from the data source.
136-
* @param string $index_field The field name to index the Model objects by.
137-
* @return array An associative array of indexed Model objects.
138-
*/
139-
public static function index_modelset_by_field(string $model_class, string $index_field): array {
140-
# First, check if this Model class has already been indexed by this field
141-
if (isset(self::$index[$model_class][$index_field])) {
142-
return self::$index[$model_class][$index_field];
143-
}
144-
145-
# Ensure the Model can be indexed by the specified field
146-
$model = new $model_class();
147-
self::ensure_model_supports_indexing($model, $index_field);
148-
149-
# Fetch or create the ModelSet for this Model class
150-
$model_set = $model->read_all();
151-
152-
# Create an associative array to hold the indexed Model objects
153-
$indexed_models = [];
154-
155-
# Index each Model object in the ModelSet by the specified field
156-
foreach ($model_set->model_objects as $model) {
157-
$index_value = $index_field === 'id' ? $model->id : $model->$index_field->value;
158-
$indexed_models[$index_value] = $model;
159-
}
160-
161-
# Store the indexed models in the ModelCache index and return them
162-
self::$index[$model_class][$index_field] = $indexed_models;
163-
return $indexed_models;
164-
}
165-
166-
/**
167-
* Checks if a given Model class has a cached Model object by its index field/value.
168-
* @param string $model_class The Model class name to check in the cache.
169-
* @param string $index_field The index field name to look up the Model object by
170-
* @param mixed $index_value The index field value to look up the Model object by
171-
* @return bool True if a cached Model object exists for the specified Model class and index
172-
*/
173-
public static function has_model(string $model_class, string $index_field = 'id', mixed $index_value = null): bool {
174-
# Cache is always a miss if the Model class is not indexed
175-
if (!self::$index[$model_class]) {
176-
return false;
177-
}
178-
179-
# Cache is a hit for non-many enabled models if a single Model object is indexed under the Model class
180-
if (self::$index[$model_class] instanceof Model) {
181-
return true;
182-
}
183-
184-
# Otherwise, cache is only a hit for many enabled models if the index field/value exists in the index
185-
return isset(self::$index[$model_class][$index_field][$index_value]);
186-
}
187-
188-
/**
189-
* Fetches a cached Model object by its Model class name and index field/value.
190-
* @param string $model_class The Model class name to load from the cache.
191-
* @param string $index_field The index field name to look up the Model object by. Only for many enabled Models.
192-
* @param mixed $index_value The index field value to look up the Model object by. Only for many enabled Models.
193-
* @return Model The cached Model object
194-
* @throws NotFoundError If no cached Model object exists for the specified Model class and index.
195-
*/
196-
public static function fetch_model(
197-
string $model_class,
198-
string $index_field = 'id',
199-
mixed $index_value = null,
200-
): Model {
201-
if (!self::has_model($model_class, $index_field, $index_value)) {
202-
throw new NotFoundError(
203-
message: "No cached Model found for Model class '$model_class' with " .
204-
"index field '$index_field' and index value '$index_value'.",
205-
response_id: 'MODEL_CACHE_MODEL_NOT_FOUND',
206-
);
207-
}
208-
209-
# For many enabled models, return the Model object indexed under the specified index field and value
210-
if (isset(self::$index[$model_class][$index_field][$index_value])) {
211-
return self::$index[$model_class][$index_field][$index_value];
212-
}
213-
214-
# Otherwise, return the Model object indexed under the Model class only
215-
return self::$index[$model_class];
216-
}
217-
21877
/**
21978
* Clears the ModelCache of all cached ModelSets and indexed Model objects.
22079
*/
22180
public static function clear(): void {
222-
self::$index = [];
22381
self::$cache = [];
22482
}
22583
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ require_once 'RESTAPI/autoloader.inc';
1313
class ModelSet {
1414
use BaseTraits;
1515

16+
/**
17+
* @var string $signature A unique signature for this ModelSet used to index this ModelSet in the query cache
18+
* while retaining all query chains. Signatures are compromised of a root signature (the Model class name) and
19+
* a series of query signatures that represent each query that has been applied to this ModelSet to this point
20+
* (separated by a colon). Example 'RESTAPI\Models\User:queryhash1:queryhash2'
21+
*/
22+
public string $signature = '';
23+
1624
/**
1725
* @var array $model_objects An array of Model objects contained by this ModelSet.
1826
*/

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APICoreModelCacheTestCase.inc

Lines changed: 0 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -83,117 +83,4 @@ class APICoreModelCacheTestCase extends TestCase {
8383
},
8484
);
8585
}
86-
87-
/**
88-
* Ensures Models must be many-enabled to be indexed in the ModelCache.
89-
*/
90-
public function test_model_index_must_be_many_enabled(): void {
91-
# Ensure attempting to index a non-many enabled Model throws an error
92-
$this->assert_throws_response(
93-
response_id: 'MODEL_CACHE_INDEX_FIELD_ON_NON_MANY_MODEL',
94-
code: 500,
95-
callable: function () {
96-
ModelCache::index_modelset_by_field(model_class: RESTAPISettings::get_class_fqn(), index_field: 'id');
97-
},
98-
);
99-
}
100-
101-
/**
102-
* Ensures we cannot index a cached ModelSet by a non-unique field.
103-
*/
104-
public function test_model_index_field_must_be_unique(): void {
105-
# Ensure indexing by id is always allowed
106-
$this->assert_does_not_throw(function () {
107-
ModelCache::index_modelset_by_field(model_class: FirewallAlias::get_class_fqn(), index_field: 'id');
108-
});
109-
110-
# Ensure attempting to index the Model by the non-unique field throws an error
111-
$this->assert_throws_response(
112-
response_id: 'MODEL_CACHE_INDEX_FIELD_NOT_UNIQUE',
113-
code: 500,
114-
callable: function () {
115-
ModelCache::index_modelset_by_field(model_class: FirewallAlias::get_class_fqn(), index_field: 'descr');
116-
},
117-
);
118-
}
119-
120-
/**
121-
* Ensures we cannot index a cached ModelSet for a Model that has a parent model class.
122-
*/
123-
public function test_model_index_field_cannot_have_parent_model_class(): void {
124-
# Ensure attempting to index the Model by the non-unique field throws an error
125-
$this->assert_throws_response(
126-
response_id: 'MODEL_CACHE_INDEX_FIELD_ON_PARENTED_MODEL',
127-
code: 500,
128-
callable: function () {
129-
ModelCache::index_modelset_by_field(
130-
model_class: DNSResolverHostOverrideAlias::get_class_fqn(),
131-
index_field: 'id',
132-
);
133-
},
134-
);
135-
}
136-
137-
/**
138-
* Ensures the index_modelset_by_field() method correctly indexes a cached ModelSet by the specified field. This
139-
* test also ensures the has_model() and fetch_model() methods work as expected for indexed Model objects.
140-
*/
141-
public function test_model_index_modelset_by_field(): void {
142-
# Create FirewallAlias model objects to test with
143-
$alias1 = new FirewallAlias(data: ['name' => 'host_alias', 'descr' => 'First alias', 'type' => 'host']);
144-
$alias2 = new FirewallAlias(data: ['name' => 'network_alias', 'descr' => 'Second alias', 'type' => 'network']);
145-
$alias3 = new FirewallAlias(data: ['name' => 'port_alias', 'descr' => 'Third alias', 'type' => 'port']);
146-
$alias1->create();
147-
$alias2->create();
148-
$alias3->create();
149-
150-
# Index the FirewallAlias ModelSet by the 'name' field
151-
ModelCache::index_modelset_by_field(model_class: FirewallAlias::get_class_fqn(), index_field: 'name');
152-
153-
# Ensure the ModelCache has the indexed Models
154-
$this->assert_is_true(
155-
ModelCache::has_model(
156-
model_class: FirewallAlias::get_class_fqn(),
157-
index_field: 'name',
158-
index_value: 'host_alias',
159-
),
160-
);
161-
$this->assert_is_true(
162-
ModelCache::has_model(
163-
model_class: FirewallAlias::get_class_fqn(),
164-
index_field: 'name',
165-
index_value: 'network_alias',
166-
),
167-
);
168-
$this->assert_is_true(
169-
ModelCache::has_model(
170-
model_class: FirewallAlias::get_class_fqn(),
171-
index_field: 'name',
172-
index_value: 'port_alias',
173-
),
174-
);
175-
176-
# Fetch the indexed Models and ensure they are correct
177-
$fetched_alias1 = ModelCache::fetch_model(
178-
model_class: FirewallAlias::get_class_fqn(),
179-
index_field: 'name',
180-
index_value: 'host_alias',
181-
);
182-
$this->assert_equals($fetched_alias1->name->value, 'host_alias');
183-
$fetched_alias2 = ModelCache::fetch_model(
184-
model_class: FirewallAlias::get_class_fqn(),
185-
index_field: 'name',
186-
index_value: 'network_alias',
187-
);
188-
$this->assert_equals($fetched_alias2->name->value, 'network_alias');
189-
$fetched_alias3 = ModelCache::fetch_model(
190-
model_class: FirewallAlias::get_class_fqn(),
191-
index_field: 'name',
192-
index_value: 'port_alias',
193-
);
194-
$this->assert_equals($fetched_alias3->name->value, 'port_alias');
195-
196-
# Clean up the created aliases
197-
FirewallAlias::delete_all();
198-
}
19986
}

0 commit comments

Comments
 (0)