@@ -20,6 +20,18 @@ class ModelCache {
2020 */
2121 public static ?self $ instance = null ;
2222
23+ /**
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+
2335 /**
2436 * @var array $cache An associative array that contains cached ModelSet objects that have been loaded into memory.
2537 * This includes root ModelSets that contain all Model objects of a given Model class as well as indexed queries
@@ -41,43 +53,185 @@ class ModelCache {
4153 }
4254
4355 /**
44- * Checks if a given Model class has a cached ModelSet .
45- * @param string $model_class The Model class name to check in the cache.
46- * @return bool True if a cached ModelSet exists for the specified Model class , false otherwise.
56+ * Checks if a cached ModelSet exists for a specified signature .
57+ * @param string $signature The signature of the ModelSet to check for in the cache.
58+ * @return bool True if a cached ModelSet exists with the specified signature , false otherwise.
4759 */
48- public static function has_modelset (string $ model_class ): bool {
49- return isset (self ::$ cache [$ model_class ]);
60+ public static function has_modelset (string $ signature ): bool {
61+ return isset (self ::$ cache [$ signature ]);
5062 }
5163
5264 /**
53- * Caches a ModelSet in the ModelCache.
54- * @param string $model_class The Model class name of the ModelSet to cache.
65+ * Caches a ModelSet in the ModelCache by its signature.
5566 * @param ModelSet $model_set The ModelSet object to cache.
5667 */
57- public static function cache_modelset (string $ model_class , ModelSet $ model_set ): void {
58- self ::$ cache [$ model_class ] = $ model_set ;
68+ public static function cache_modelset (ModelSet $ model_set ): void {
69+ # Do not allow ModelSets to be cached without a valid signature
70+ if (!$ model_set ->signature ) {
71+ throw new ServerError (
72+ message: 'ModelSet cannot be cached without a valid signature. ' ,
73+ response_id: 'MODEL_CACHE_MODELSET_MISSING_SIGNATURE ' ,
74+ );
75+ }
76+
77+ # Index the ModelSet in the cache by its signature
78+ self ::$ cache [$ model_set ->signature ] = $ model_set ;
5979 }
6080
6181 /**
6282 * Fetches a cached ModelSet by its Model class name.
63- * @param string $model_class The Model class name to load from the cache.
83+ * @param string $signature The signature of the ModelSet to fetch from the cache.
6484 * @return ModelSet The cached ModelSet
65- * @throws NotFoundError If no cached ModelSet exists for the specified Model class .
85+ * @throws NotFoundError If no cached ModelSet exists for the specified signature .
6686 */
67- public static function fetch_modelset (string $ model_class ): ModelSet {
68- if (!self ::has_modelset ($ model_class )) {
87+ public static function fetch_modelset (string $ signature ): ModelSet {
88+ if (!self ::has_modelset ($ signature )) {
6989 throw new NotFoundError (
70- message: "No cached ModelSet found for Model class ' $ model_class '. " ,
90+ message: "No cached ModelSet found with signature ' $ signature '. " ,
7191 response_id: 'MODEL_CACHE_MODELSET_NOT_FOUND ' ,
7292 );
7393 }
74- return self ::$ cache [$ model_class ];
94+ return self ::$ cache [$ signature ];
95+ }
96+
97+ /**
98+ * Raises an error if the given Model object does not support being indexed by the specified field.
99+ * @param Model $model The Model object to check for uniqueness.
100+ * @param string $index_field The index field name to check for uniqueness.
101+ * @throws ServerError If the Model is attempting to be indexed by a non-unique field.
102+ * @throws ServerError If the Model is not many-enabled.
103+ */
104+ private static function ensure_model_supports_indexing (Model $ model , string $ index_field ): void {
105+ # Only many-enabled Models can be indexed
106+ if (!$ model ->many ) {
107+ throw new ServerError (
108+ message: "Cannot index Model class ' " . $ model ->get_class_fqn () . 'because it is not many-enabled. ' ,
109+ response_id: 'MODEL_CACHE_INDEX_FIELD_ON_NON_MANY_MODEL ' ,
110+ );
111+ }
112+
113+ # Models with parent model classes cannot be indexed
114+ if ($ model ->parent_model_class ) {
115+ throw new ServerError (
116+ message: "Cannot index Model class ' " .
117+ $ model ->get_class_fqn () .
118+ "' because it has a parent model class ' " .
119+ $ model ->parent_model_class .
120+ "'. " ,
121+ response_id: 'MODEL_CACHE_INDEX_FIELD_ON_PARENTED_MODEL ' ,
122+ );
123+ }
124+
125+ # If indexing by 'id', it's always unique
126+ if ($ index_field === 'id ' ) {
127+ return ;
128+ }
129+
130+ # Check if the index field is unique on the Model object
131+ if (!$ model ->$ index_field ->unique ) {
132+ throw new ServerError (
133+ message: "Cannot index Model class ' " .
134+ $ model ->get_class_fqn () .
135+ "' by non-unique field " .
136+ "' $ index_field'. " ,
137+ response_id: 'MODEL_CACHE_INDEX_FIELD_NOT_UNIQUE ' ,
138+ );
139+ }
140+ }
141+
142+ /**
143+ * Indexes the ModelSet cache for a given Model class by a specified field. This method will populate
144+ * the $index array for the specified Model class and index field.
145+ * @param string $model_class The Model class name whose ModelSet is to be indexed. If no cached ModelSet exists,
146+ * one will be created by reading all Model objects from the data source.
147+ * @param string $index_field The field name to index the Model objects by.
148+ * @return array An associative array of indexed Model objects.
149+ */
150+ public static function index_modelset_by_field (string $ model_class , string $ index_field ): array {
151+ # First, check if this Model class has already been indexed by this field
152+ if (isset (self ::$ index [$ model_class ][$ index_field ])) {
153+ return self ::$ index [$ model_class ][$ index_field ];
154+ }
155+
156+ # Ensure the Model can be indexed by the specified field
157+ $ model = new $ model_class ();
158+ self ::ensure_model_supports_indexing ($ model , $ index_field );
159+
160+ # Fetch or create the ModelSet for this Model class
161+ $ model_set = $ model ->read_all ();
162+
163+ # Create an associative array to hold the indexed Model objects
164+ $ indexed_models = [];
165+
166+ # Index each Model object in the ModelSet by the specified field
167+ foreach ($ model_set ->model_objects as $ model ) {
168+ $ index_value = $ index_field === 'id ' ? $ model ->id : $ model ->$ index_field ->value ;
169+ $ indexed_models [$ index_value ] = $ model ;
170+ }
171+
172+ # Store the indexed models in the ModelCache index and return them
173+ self ::$ index [$ model_class ][$ index_field ] = $ indexed_models ;
174+ return $ indexed_models ;
175+ }
176+
177+ /**
178+ * Checks if a given Model class has a cached Model object by its index field/value.
179+ * @param string $model_class The Model class name to check in the cache.
180+ * @param string $index_field The index field name to look up the Model object by
181+ * @param mixed $index_value The index field value to look up the Model object by
182+ * @return bool True if a cached Model object exists for the specified Model class and index
183+ */
184+ public static function has_model (string $ model_class , string $ index_field = 'id ' , mixed $ index_value = null ): bool {
185+ # Cache is always a miss if the Model class is not indexed
186+ if (!self ::$ index [$ model_class ]) {
187+ return false ;
188+ }
189+
190+ # Cache is a hit for non-many enabled models if a single Model object is indexed under the Model class
191+ if (self ::$ index [$ model_class ] instanceof Model) {
192+ return true ;
193+ }
194+
195+ # Otherwise, cache is only a hit for many enabled models if the index field/value exists in the index
196+ return isset (self ::$ index [$ model_class ][$ index_field ][$ index_value ]);
197+ }
198+
199+ /**
200+ * Fetches a cached Model object by its Model class name and index field/value.
201+ * @param string $model_class The Model class name to load from the cache.
202+ * @param string $index_field The index field name to look up the Model object by. Only for many enabled Models.
203+ * @param mixed $index_value The index field value to look up the Model object by. Only for many enabled Models.
204+ * @return Model The cached Model object
205+ * @throws NotFoundError If no cached Model object exists for the specified Model class and index.
206+ */
207+ public static function fetch_model (
208+ string $ model_class ,
209+ string $ index_field = 'id ' ,
210+ mixed $ index_value = null ,
211+ ): Model {
212+ if (!self ::has_model ($ model_class , $ index_field , $ index_value )) {
213+ throw new NotFoundError (
214+ message: "No cached Model found for Model class ' $ model_class' with " .
215+ "index field ' $ index_field' and index value ' $ index_value'. " ,
216+ response_id: 'MODEL_CACHE_MODEL_NOT_FOUND ' ,
217+ );
218+ }
219+
220+ # For many enabled models, return the Model object indexed under the specified index field and value
221+ if (isset (self ::$ index [$ model_class ][$ index_field ][$ index_value ])) {
222+ return self ::$ index [$ model_class ][$ index_field ][$ index_value ];
223+ }
224+
225+ # Otherwise, return the Model object indexed under the Model class only
226+ return self ::$ index [$ model_class ];
75227 }
76228
229+
77230 /**
78231 * Clears the ModelCache of all cached ModelSets and indexed Model objects.
79232 */
80233 public static function clear (): void {
81234 self ::$ cache = [];
235+ self ::$ index = [];
82236 }
83237}
0 commit comments