| @@ -104,8 +104,15 @@ | ||
| 104 | 104 | |
| 105 | 105 | /** |
| 106 | 106 | * Get SEO settings for a specific context |
| 107 | 107 | * |
| 108 | + * The returned array always carries every key the context defines: the | |
| 109 | + * saved rows are merged OVER the context defaults, so callers can read a | |
| 110 | + * key without checking whether it exists. That also means the result never | |
| 111 | + * distinguishes "the user saved this" from "this is the built-in default" — | |
| 112 | + * when a caller needs that distinction (an audit asking whether anything | |
| 113 | + * was configured at all), use get_stored_settings() instead. | |
| 114 | + * | |
| 108 | 115 | * @since 1.0.0 |
| 109 | 116 | * |
| 110 | 117 | * @param string $context_type The context type |
| 111 | 118 | * @param int|null $context_id Optional. Context ID |
| @@ -117,14 +124,65 @@ | ||
| 117 | 124 | if (!in_array($context_type, $this->get_supported_contexts(), true)) { |
| 118 | 125 | return $this->get_default_settings($context_type); |
| 119 | 126 | } |
| 120 | 127 | |
| 128 | + // Serve from the object cache when available. This runs on every front-end | |
| 129 | + // request (the_content, thumbnails), so avoiding a DB hit per request matters. | |
| 130 | + $cache_key = $this->get_cache_key($context_type, $context_id); | |
| 131 | + $cached = wp_cache_get($cache_key, 'thinkrank_seo'); | |
| 132 | + if (is_array($cached)) { | |
| 133 | + return $cached; | |
| 134 | + } | |
| 135 | + | |
| 136 | + // Merge with defaults to ensure all required keys exist | |
| 137 | + $merged = array_merge( | |
| 138 | + $this->get_default_settings($context_type), | |
| 139 | + $this->get_stored_settings($context_type, $context_id) | |
| 140 | + ); | |
| 141 | + | |
| 142 | + // Cache the resolved settings; invalidated on every save via clear_cache(). | |
| 143 | + wp_cache_set($cache_key, $merged, 'thinkrank_seo'); | |
| 144 | + | |
| 145 | + return $merged; | |
| 146 | + } | |
| 147 | + | |
| 148 | + /** | |
| 149 | + * Get ONLY the settings actually saved for a context — no defaults merged. | |
| 150 | + * | |
| 151 | + * get_settings() layers the context defaults under the stored rows, which | |
| 152 | + * makes "has the user configured anything?" unanswerable through it: a site | |
| 153 | + * with zero saved rows still gets back a fully populated array. Any audit | |
| 154 | + * or first-run check that must tell a configured site from an untouched one | |
| 155 | + * has to read the storage layer directly, which is what this exposes. | |
| 156 | + * | |
| 157 | + * Returns an empty array when nothing has been saved for the context, or | |
| 158 | + * when the context type is not supported by this manager. | |
| 159 | + * | |
| 160 | + * Note this is the storage layer, not the settings a manager reports: a | |
| 161 | + * subclass that overrides get_settings() to layer in another source — | |
| 162 | + * Schema_Management_System fills its logo and organization fields from Site | |
| 163 | + * Identity, Social_Meta_Manager has its own override — contributes nothing | |
| 164 | + * here. Read it to ask what the site saved, never to read a value out. | |
| 165 | + * | |
| 166 | + * @since 2.3.1 | |
| 167 | + * | |
| 168 | + * @param string $context_type The context type | |
| 169 | + * @param int|null $context_id Optional. Context ID | |
| 170 | + * @return array Saved settings, keyed by setting key. Empty when nothing is stored. | |
| 171 | + */ | |
| 172 | + public function get_stored_settings(string $context_type, ?int $context_id = null): array { | |
| 173 | + $context_type = sanitize_key($context_type); | |
| 174 | + | |
| 175 | + if (!in_array($context_type, $this->get_supported_contexts(), true)) { | |
| 176 | + return []; | |
| 177 | + } | |
| 178 | + | |
| 121 | 179 | // Convert NULL context_id to 0 for site-wide settings to match save behavior |
| 122 | 180 | $db_context_id = $context_id === null ? 0 : $context_id; |
| 123 | 181 | |
| 124 | - // Serve from the object cache when available. This runs on every front-end | |
| 125 | - // request (the_content, thumbnails), so avoiding a DB hit per request matters. | |
| 126 | - $cache_key = $this->get_cache_key($context_type, $context_id); | |
| 182 | + // Cached under its own key so the merged and unmerged views can never be | |
| 183 | + // served for one another. clear_cache() drops both on every save. | |
| 184 | + $cache_key = $this->get_stored_cache_key($context_type, $context_id); | |
| 127 | 185 | $cached = wp_cache_get($cache_key, 'thinkrank_seo'); |
| 128 | 186 | if (is_array($cached)) { |
| 129 | 187 | return $cached; |
| 130 | 188 | } |
| @@ -151,9 +209,9 @@ | ||
| 151 | 209 | ARRAY_A |
| 152 | 210 | ); |
| 153 | 211 | |
| 154 | 212 | $settings = []; |
| 155 | - foreach ($results as $row) { | |
| 213 | + foreach ((array) $results as $row) { | |
| 156 | 214 | $value = maybe_unserialize($row['setting_value']); |
| 157 | 215 | |
| 158 | 216 | // Ensure proper data type conversion for boolean fields |
| 159 | 217 | if (in_array($row['setting_key'], $this->boolean_setting_keys(), true)) { |
| @@ -172,15 +230,11 @@ | ||
| 172 | 230 | |
| 173 | 231 | $settings[$row['setting_key']] = $value; |
| 174 | 232 | } |
| 175 | 233 | |
| 176 | - // Merge with defaults to ensure all required keys exist | |
| 177 | - $merged = array_merge($this->get_default_settings($context_type), $settings); | |
| 234 | + wp_cache_set($cache_key, $settings, 'thinkrank_seo'); | |
| 178 | 235 | |
| 179 | - // Cache the resolved settings; invalidated on every save via clear_cache(). | |
| 180 | - wp_cache_set($cache_key, $merged, 'thinkrank_seo'); | |
| 181 | - | |
| 182 | - return $merged; | |
| 236 | + return $settings; | |
| 183 | 237 | } |
| 184 | 238 | |
| 185 | 239 | /** |
| 186 | 240 | * Save SEO settings for a specific context |
| @@ -698,8 +752,12 @@ | ||
| 698 | 752 | protected function clear_cache(string $context_type, ?int $context_id): void { |
| 699 | 753 | $cache_key = $this->get_cache_key($context_type, $context_id); |
| 700 | 754 | wp_cache_delete($cache_key, 'thinkrank_seo'); |
| 701 | 755 | |
| 756 | + // The defaults-free view is cached separately, so a save has to drop it | |
| 757 | + // too or get_stored_settings() keeps answering with the pre-save rows. | |
| 758 | + wp_cache_delete($this->get_stored_cache_key($context_type, $context_id), 'thinkrank_seo'); | |
| 759 | + | |
| 702 | 760 | // Clear related transients |
| 703 | 761 | delete_transient("thinkrank_seo_{$this->manager_type}_{$context_type}_{$context_id}"); |
| 704 | 762 | } |
| 705 | 763 | |
| @@ -750,8 +808,26 @@ | ||
| 750 | 808 | // (which pass 0) resolve to the SAME cache entry — otherwise a save would |
| 751 | 809 | // never invalidate the value a front-end read cached. |
| 752 | 810 | $db_context_id = $context_id === null ? 0 : $context_id; |
| 753 | 811 | return "seo_settings_{$this->manager_type}_{$context_type}_{$db_context_id}"; |
| 812 | + } | |
| 813 | + | |
| 814 | + /** | |
| 815 | + * Cache key for the defaults-free view of a context. | |
| 816 | + * | |
| 817 | + * Deliberately distinct from get_cache_key(): the two views hold different | |
| 818 | + * data (one merged with defaults, one only what was saved), so sharing an | |
| 819 | + * entry would let whichever ran first answer for the other. | |
| 820 | + * | |
| 821 | + * @since 2.3.1 | |
| 822 | + * | |
| 823 | + * @param string $context_type The context type | |
| 824 | + * @param int|null $context_id Optional. Context ID | |
| 825 | + * @return string | |
| 826 | + */ | |
| 827 | + protected function get_stored_cache_key(string $context_type, ?int $context_id): string { | |
| 828 | + $db_context_id = $context_id === null ? 0 : $context_id; | |
| 829 | + return "seo_stored_settings_{$this->manager_type}_{$context_type}_{$db_context_id}"; | |
| 754 | 830 | } |
| 755 | 831 | |
| 756 | 832 | /** |
| 757 | 833 | * Ensure settings table exists |