feat: add per-model TTL via HasLadaTtl interface#152
Open
acaliskol wants to merge 2 commits into
Open
Conversation
Adds per-model cache TTL overrides without breaking the global
`expiration_time` default. Models can opt-in either via the new
`HasLadaTtl` interface (most flexible) or via a static config map.
Resolution order (first non-null wins):
1. Model implements `HasLadaTtl` → `$model->getLadaTtl()`
2. `config('lada-cache.model_ttls.<FQCN>')` static map
3. `config('lada-cache.expiration_time')` global default
Semantics of TTL values:
- `null` defer to fallback layer
- `> 0` TTL in seconds
- `0` persist forever (cache until tag invalidation) — matches
how global `expiration_time = 0` already behaves
- `< 0` same as 0 (forever) — discouraged
Why an interface and not a `$ladaTtl` public property:
- Traits cannot enforce property type across consumers; a public
property would collide with any model column named `ladaTtl`
- Octane-safe: no mutable shared state on a long-lived model instance
- PHPStan-narrowable: `instanceof HasLadaTtl` gives full type info,
whereas `property_exists($model, 'ladaTtl')` does not narrow
Files:
- src/Contracts/HasLadaTtl.php - new interface
- src/TtlResolver.php - new Octane-safe resolver service
- src/Cache.php - `set(..., ?int $ttl = null)` now accepts per-call TTL
- src/QueryHandler.php - resolves model TTL before each cache write
- src/LadaCacheServiceProvider.php - registers `lada.ttl_resolver` singleton
- config/lada-cache.php - new `model_ttls` config section
- tests/Integration/Cache/PerModelTtlTest.php - 9 tests covering resolver +
Cache::set TTL semantics (explicit / zero / null fallback)
4 tasks
Models can now declare `public ?int $ladaTtl = X;` instead of writing a
custom `getLadaTtl()` method. LadaCacheTrait now provides a default
implementation that reads from the property, so the typical case becomes
a single line:
class City extends Model implements HasLadaTtl
{
use LadaCacheTrait;
public ?int $ladaTtl = 86400 * 30; // 30 days
}
The method override remains available for dynamic TTL logic.
Why the property is declared on the model and not on the trait: PHP does
not allow a subclass to redeclare a trait-provided property with a
different default value (Fatal error: incompatible composition). The
trait therefore supplies only the method and lets the model own the
property.
Defensive read: a typed property without a default value
(`public ?int $ladaTtl;`) throws Error("must not be accessed before
initialization") on direct access. The trait guards with
ReflectionProperty::isInitialized() so the resolver falls through to the
config-level fallback instead of crashing.
Backwards compatible: existing models that override getLadaTtl() are
untouched; the trait method is a default override candidate.
Tests added in tests/Integration/Cache/PerModelTtlTest.php cover:
- PropertyOnly — property only, no method override
- PropertyNull — property defaults to null, falls back to config
- PropertyMissing — implements HasLadaTtl without declaring property
- PropertyUninit — typed property with no default value
Owner
|
Hi @acaliskol, thanks for your contribution! What are the use cases for which you think this new feature would be helpful? In my opinion it kind of contradicts with the main promise of Lada Cache that the user shouldn't need to deal with invalidation. |
Owner
|
Tagging some contributors here to discuss this feature proposal. Let me know what you guys think! Is this the right approach in your opinion? I'd like to get the community more involved for directional changes like this. @kontainer-dam-pim @Tim-streamline @zgetro @duyphuongn @MGApcDev @michael-rubel @ogunsakin01@diegotibi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds per-model cache TTL overrides without breaking the global
expiration_timedefault. Models opt in via the newHasLadaTtlinterface, andLadaCacheTraitprovides a defaultgetLadaTtl()implementation so the common case is a single property declaration:Resolution order
First non-null wins:
HasLadaTtl→\$model->getLadaTtl()LadaCacheTraitreadspublic ?int \$ladaTtlconfig('lada-cache.model_ttls.<FQCN>')config('lada-cache.expiration_time')globalTTL value semantics
null> 00expiration_time = 0behavior< 0Why the property is on the model, not on the trait
PHP does not allow a subclass to redeclare a trait-provided property with a different default value:
So
LadaCacheTraitcannot shippublic ?int \$ladaTtl = null;itself. Instead the trait supplies only the method and lets each model declare the property with whatever default it needs.property_exists()inside the trait method handles models that implementHasLadaTtlwithout declaring the property.Defensive read for uninitialized typed properties
public ?int \$ladaTtl;(typed, no default) is legal PHP.property_exists()returnstrue, but reading the property before any assignment throws:The trait method uses
ReflectionProperty::isInitialized()to detect this state and returnsnull, letting the resolver fall through to the config-level fallback instead of crashing the request.Files
src/Contracts/HasLadaTtl.php— new interfacesrc/Database/LadaCacheTrait.php— defaultgetLadaTtl()reading\$ladaTtlproperty with uninitialized guardsrc/TtlResolver.php— new Octane-safe resolver service (singleton, no mutable state)src/Cache.php—set(..., ?int \$ttl = null)now accepts per-call TTL. Backward compatible default.src/QueryHandler.php— resolves model TTL before eachcache->set()src/LadaCacheServiceProvider.php— registerslada.ttl_resolversingletonconfig/lada-cache.php— newmodel_ttlsconfig section with examplesTests
13 tests in
tests/Integration/Cache/PerModelTtlTest.php:TtlResolver fallback chain (interface-based):
LadaCacheTrait property-based default
getLadaTtl():Cache::set TTL semantics:
TTLcommand)TTLreturns -1 (no expiration)expiration_timeCompatibility
Cache::set()4th parameter is optional with?int \$ttl = nulldefault → no break for existing callerslada.ttl_resolversingleton is additive; provider'sprovides()array updatedmodel_ttlsentry behave exactly as beforegetLadaTtl()explicitly continue to take precedence over the trait's default impl