itjonction / blockcache
适用于Laravel和纯PHP的块级缓存
Requires
- illuminate/support: >=10.0 <13.0
Requires (Dev)
- illuminate/cache: >=10.0 <13.0
- illuminate/config: ^10.0 || ^13.0
- illuminate/container: ^10.0 || ^13.0
- illuminate/database: >=10.0 <13.0
- mockery/mockery: 2.0.x-dev
- monolog/monolog: ^2.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2024-09-07 16:08:51 UTC
README
Blockcache是一个为Laravel提供的视图逻辑嵌套块级缓存的包。
Laravel安装
步骤2:服务提供者
对于您的Laravel应用,打开config/app.php,在providers数组中追加
Itjonction\Blockcache\BlockcacheServiceProvider::class
这将引导包进入Laravel。
步骤3:缓存驱动
为了使此包正常工作,您必须使用支持标签的Laravel缓存驱动(如Cache::tags('foo'))。Memcached和Redis等驱动支持此功能。
检查您的.env文件,确保您的CACHE_DRIVER选择符合此要求
CACHE_DRIVER=memcached
如果您需要任何帮助,请参阅Laravel的缓存配置文档。
用法
基础知识
现在包已安装,您可以在视图中任何位置使用提供的@cache Blade指令,如下所示
@cache('my-cache-key')
<div>
<h1>Hello World</h1>
</div>
@endcache
通过使用@cache和@endcache指令包围此HTML块,您指示包缓存给定的HTML。虽然这个例子很简单,但您可以想象更复杂的视图,具有嵌套缓存和延迟加载的关系调用,这些调用会触发额外的数据库查询。在缓存HTML片段的初始页面加载后,每次刷新都会从缓存中获取,从而防止额外的数据库查询。
在生产环境中,这将无限期地缓存HTML片段。对于本地开发,相关的缓存将在每次刷新页面时自动刷新,这样您就可以更新视图和模板,而无需手动清除缓存。
旧模板和类
虽然此包依赖于Laravel类,但Laravel不需要引导。要在非Laravel模板中使用此库,请执行以下操作以直接使用Blockcache
use Itjonction\Blockcache\General\CacheManager; use Illuminate\Cache\Repository; use Illuminate\Redis\RedisManager; use Illuminate\Cache\RedisStore; use Illuminate\Foundation\Application; // Configure Redis connection settings $config = [ 'default' => [ 'url' => env('REDIS_URL', null), 'host' => env('REDIS_HOST', '127.0.0.1'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), 'database' => env('REDIS_DB', 0), ], ]; // Create the Redis manager instance $redisManager = new RedisManager($app, 'predis', ['default' => $config['default']]); // Create the Redis store instance $redisStore = new RedisStore($redisManager, 'cache'); // Create the Cache repository instance $cache = new Repository($redisStore); $cacheManager = new CacheManager($cache); if (! $cacheManager->startCache('my-cache-key') ){ echo "<div>view fragment</div>"; } $output = $cacheManager->endCache();
或者,即使是在旧代码中,您也可以引导Laravel应用程序实例
// php/bootstrap/legacy/laravel.php require_once __DIR__ . '/../../vendor/autoload.php'; use Illuminate\Cache\CacheManager; use Illuminate\Container\Container; use Illuminate\Events\Dispatcher; use Illuminate\Filesystem\Filesystem; use Illuminate\Config\Repository as ConfigRepository; use Illuminate\Redis\RedisManager; $container = new Container; // Set up the event dispatcher $events = new Dispatcher($container); $container->instance('events', $events); // Set up the configuration $config = new ConfigRepository([ 'app' => require __DIR__ . '/../../config/app.php', 'cache' => require __DIR__ . '/../../config/legacy/cache.php', 'database' => require __DIR__ . '/../../config/legacy/database.php', ]); $container->instance('config', $config); $files = new Filesystem; $container->instance('files', $files); // Set up the Redis manager $redisConfig = $config->get('database.redis'); $redisManager = new RedisManager($container, $redisConfig['client'], $redisConfig); $container->instance('redis', $redisManager); // Set up the Cache manager $cacheManager = new CacheManager($container); $container->instance('cache', $cacheManager); return $container;
这允许您缓存任何视图片段,无论它是否是Blade模板。
use Itjonction\Blockcache\General\CacheManager; use Illuminate\Foundation\Application; $container = require_once __DIR__ . '/../path/to/your/bootstrap/legacy/laravel.php'; // Create the Cache repository instance $cacheManager = new CacheManager($container->make('cache')->store('redis')); if (! $cacheManager->startCache('my-cache-key') ){ echo "<div>view fragment</div>"; } $output = $cacheManager->endCache();
由于您的生产服务器将无限期地缓存片段,请将清除相关缓存的操作添加到您的部署流程中
Cache::tags('views')->flush();
缓存模型
虽然您可以随意为缓存键硬编码任何字符串,但俄罗斯套娃式缓存的真正威力在于使用缓存失效策略,例如基于时间的策略。
考虑以下片段
@cache($post)
<article>
<h2>{{ $post->title }}</h2>
<p>Written By: {{ $post->author->username }}</p>
<div class="body">{{ $post->body }}</div>
</article>
@endcache
在这个例子中,我们向@cache指令传递了$post对象而不是字符串。该包将在模型上查找getCacheKey()方法。为了启用此功能,让您的Eloquent模型使用Itjonction\Blockcache\HasCacheKey特质
use Itjonction\Blockcache\HasCacheKey; class Post extends Eloquent { use HasCacheKey; }
或者,您可以在您的Eloquent模型扩展的父类上使用此特质。
现在,此片段的缓存键将包含对象的id和updated_at时间戳:App\Post/1-13241235123。
关键是,由于我们将
updated_at时间戳纳入缓存键,每次更新帖子时,缓存键都会更改,从而有效地清除缓存。
现在,您可以像这样渲染视图
resources/views/cards/_card.blade.php
@cache($card)
<article class="Card">
<h2>{{ $card->title }}</h2>
<ul>
@foreach ($card->notes as $note)
@include ('cards/_note')
@endforeach
</ul>
</article>
@endcache
resources/views/cards/_note.blade.php
@cache($note)
<li>{{ $note->body }}</li>
@endcache
注意我们缓存的俄罗斯套娃式级联;如果任何笔记被更新,其单独的缓存将清除,以及其父缓存,但任何兄弟姐妹将保持不变。
旧写通过缓存
因为写入缓存依赖于数据库中的update_at字段,如果该字段不存在,您需要添加它。为了保持旧项目中的updated_at字段准确,您可以使用数据库触发器。以下是一个简单的方法:
-
创建数据库触发器:编写一个触发器,在每个更新操作中更新
updated_at字段。这确保了字段始终被更新,无论更新来自何处。 -
MySQL触发器示例:如果您使用MySQL,以下是一个基本示例
DELIMITER // CREATE TRIGGER update_timestamp BEFORE UPDATE ON your_table_name FOR EACH ROW BEGIN SET NEW.updated_at = NOW(); END// DELIMITER ;
-
添加Eloquent配置:确保您的模型正确使用
updated_at和created_at字段。默认情况下,Eloquent期望这些字段。class YourModel extends Model { public $timestamps = true; }
-
更新旧代码:逐步重构您的旧代码,使其在可能的情况下使用Eloquent进行数据库操作。
-
这将使管理和维护时间戳更容易。
-
手动更新:对于无法立即重构的应用程序部分,确保在SQL查询中手动更新
updated_at字段。UPDATE your_table_name SET column1 = value1, updated_at = NOW() WHERE condition;
通过使用数据库触发器和逐步重构旧代码,您可以确保updated_at字段保持准确和一致。
触控
为了使此技术正常工作,我们需要一种机制来在每次更新模型时通知父级关系(随后清除父级缓存)。以下是基本工作流程:
- 模型在数据库中更新。
- 它的
updated_at时间戳被刷新,触发了实例的新缓存键。 - 模型“触控”(或ping)其父级。
- 父级的
updated_at时间戳被更新,清除其相关缓存。 - 只有受影响的片段重新渲染。所有其他缓存项保持不变。
Laravel提供开箱即用的“touch”功能。考虑一个需要每次更新时通知其父级Card关系的Note对象。
<?php namespace App; use Itjonction\Blockcache\HasCacheKey; use Illuminate\Database\Eloquent\Model; class Note extends Model { use HasCacheKey; protected $touches = ['card']; public function card() { return $this->belongsTo(Card::class); } }
$touches = ['card']部分指示Laravel在笔记更新时pingcard关系的时间戳。
旧版touch()方法
对于不使用Eloquent的旧代码,您需要在类的逻辑中编写更新父级数据库中updated_at字段的能力。这将确保数据更改时缓存键被更新。
所有无效化策略
@cache($key)指令将从缓存中检索内容或为指定内容创建新的缓存条目。通过操作缓存键,您可以实现各种缓存策略。
这些策略的秘诀是使用由HasCacheKey特质提供的缓存实用程序类,该特质应添加到您想要使用块缓存的类中。特质包括用于知名缓存无效化策略的方法。
您可以使用键值存储以关联数组的形式实现各种缓存无效化策略,作为Blade指令的第二个参数。以下是策略:
写入缓存
当缓存中的数据更改时更新缓存键。此策略依赖于使用模型的updated_at时间戳和触控父级模型的HasCacheKey特质。
@cache($eloquentModel->getCacheKey()) <div>view fragment</div> @endcache
手动无效化:完成
需要明确操作来清除或刷新缓存。这是默认行为。
@cache('my-unique-key')
<div>view fragment</div>
@endcache
要手动清除此缓存,请使用以下操作(views是默认标签)
Cache::tags('views')->flush();
生存时间(TTL):完成
自动在设置秒数的期间后过期缓存内容。
@cache('my-unique-key', ['ttl' => 60])
<div>view fragment</div>
@endcache
或者,您可以通过设置范围将TTL设置为随机周期
@cache('my-unique-key', ['ttl' => [60, 120]])
<div>view fragment</div>
@endcache
当缓存多个片段时,这将确保它们不会同时过期。
缓存标签:完成
标签将相关内容组合在一起,允许进行分组无效化。
@cache('my-unique-key', ['tags' => ['tag1', 'tag2']])
<div>view fragment</div>
@endcache
理解Laravel中的缓存标签
缓存标签:
- 允许您为一个缓存项分配多个标签。
- 提供了一种将相关缓存项分组并进行批量操作(例如,使具有特定标签的所有项失效)的方式。
缓存标签是如何工作的
当您使用标签时,实际上创建了一个包含所有指定标签的复合键。这意味着当您存储具有多个标签的项时,您必须使用相同的标签集来检索它。
示例
如果您使用标签['orders', 'invoices']存储一个项,缓存系统内部创建一个代表这些标签组合的键。要检索此项,您必须指定这两个标签。
使用标签存储和检索
当您存储一个项时
$this->cache->tags(['orders', 'invoices'])->put('my-unique-key', $fragment, $ttl);
要检索它,您必须使用
$this->cache->tags(['orders', 'invoices'])->get('my-unique-key');
如果您尝试使用单个标签或不同的组合来检索它,它将找不到该项。
测试缓存标签
-
通过测试:这是因为您检查了具有确切标签组合的键的存在。
$this->assertTrue($this->cacheManager->has('my-unique-key',['orders','invoices']));
-
失败测试:这是因为您使用单个标签进行检查,这与复合键不匹配。
$this->assertTrue($this->cacheManager->has('my-unique-key','orders')); $this->assertTrue($this->cacheManager->has('my-unique-key','invoices'));
为什么会发生这种情况?
当您使用
$this->cache->tags(['orders', 'invoices'])->put('my-unique-key', $fragment, $ttl);
- 它将项存储在由
['orders', 'invoices']生成的复合键下。
当您检查
$this->cache->has('my-unique-key', 'orders'); // Incorrect $this->cache->has('my-unique-key', 'invoices'); // Incorrect
- 这些检查找不到项,因为它存储在复合键下,而不是在每个单独的标签下。
测试的正确方法
为了正确测试具有多个标签的缓存,始终使用存储期间使用的确切标签组合
测试多个标签
public function test_it_handles_multiple_tags() { $directive = $this->createNewCacheDirective(); $directive->setUp('my-unique-key', ['tags' => ['orders','invoices']]); echo "<div>view tags</div>"; $directive->tearDown(); $options = $directive->getOptions(); $this->assertIsArray($options, 'Options should be an array.'); $this->assertArrayHasKey('tags', $options, 'Options should contain a tags key.'); $this->assertIsArray($options['tags'], 'Tags should be an array.'); // Check using the exact combination of tags $this->assertTrue($this->cacheManager->has('my-unique-key', ['orders', 'invoices'])); }
批量操作和失效
使用标签使缓存项失效
当您使用标签使缓存项失效时,它会影响包含那些标签的所有项。
示例:如果您有一个带有['orders', 'invoices']标签的项,并且使orders失效,它也会使带有orders和invoices的项失效。
代码示例:
Cache::tags('orders')->flush();
这将使以下项失效
- 带有
['orders']的标签的项 - 带有
['orders', 'invoices']的标签的项 - 包括
orders的任何其他组合
说明:
- 复合键:了解标签创建复合键。
- 一致性:存储和检索时使用相同的标签组合。
- 批量操作:使用标签高效地管理缓存项组。
- 失效:使单个标签失效将影响包含该标签的所有项,即使它们有其他标签。
通过理解和正确使用缓存标签,您可以有效地分组、管理和使相关缓存项失效。始终记住在存储和检索缓存项时使用确切的标签组合,并注意使标签失效将影响包含该标签的所有项,即使它们有其他标签。
内容版本控制:完成
使用版本号强制在每次发布时更新缓存。
@cache('my-unique-key', ['version' => 'v1'])
<div>view fragment</div>
@endcache
Stale-While-Revalidate:待办
在异步更新缓存的同时提供陈旧内容。
@cache('my-unique-key', ['stale-while-revalidate' => true])
<div>view fragment</div>
@endcache
条件请求:待办
使用HTTP头在提供内容之前验证缓存的新鲜度。
@cache('my-unique-key', ['conditional' => true])
<div>view fragment</div>
@endcache
基于事件的失效:暂停:由于旧代码缺乏事件支持而受阻
基于特定事件触发缓存失效。
@cache('my-unique-key', ['event' => 'modelUpdated'])
<div>view fragment</div>
@endcache
旧版失效策略
所有策略都可用于您的旧代码,即使您没有使用Laravel。
$cacheManager->startCache('my-cache-key', ['ttl' => 60]);
缓存集合
您可能还希望缓存Laravel集合
@cache($posts)
@foreach ($posts as $post)
@include ('post')
@endforeach
@endcache
只要$posts集合的内容不改变,那个@foreach部分就永远不会运行。相反,我们将从缓存中提取。
在幕后,此包将检测您是否将 Laravel 集合传递给 cache 指令,并为该集合生成一个唯一的缓存键。
常见问题解答
1. 有没有方法可以覆盖模型实例的缓存键?
是的。例如
@cache('my-custom-key')
<div>view here</div>
@endcache
只需提供一个字符串,而不是模型,即可指示包使用 my-custom-key 作为缓存键。
添加和配置日志记录器
要使用此包提供的日志记录功能,请按照以下步骤在 Laravel 应用程序中配置日志记录器。此包利用 Monolog 进行日志记录,并与 Laravel 的日志系统无缝集成。
1. 安装 Monolog
确保 Monolog 已包含在您的 composer.json 文件中。如果尚未包含,请通过运行以下命令将其添加到项目中:
composer require monolog/monolog
2. 在 Laravel 中配置日志记录器
打开 config/logging.php 文件,并添加一个新的自定义日志通道。以下示例演示了如何使用 Monolog 的 StreamHandler 创建自定义日志通道。
return [ 'default' => env('LOG_CHANNEL', 'stack'), 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['single'], ], 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), 'level' => 'debug', ], 'custom' => [ 'driver' => 'monolog', 'handler' => Monolog\Handler\StreamHandler::class, 'with' => [ 'stream' => storage_path('logs/custom.log'), 'level' => Monolog\Logger::DEBUG, ], ], ], ];
此配置定义了一个 custom 日志通道,将日志消息写入 storage/logs/custom.log。
3. 注入并使用自定义日志记录器
在您的应用程序中,您可以根据需要注入并使用自定义日志记录器。以下是如何将日志记录器注入到控制器中的示例。
<?php namespace App\Http\Controllers; use Psr\Log\LoggerInterface; class ExampleController extends Controller { protected $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function index() { $this->logger->info('This is a custom log message.'); } }
4. 包中的示例用法
在使用此包时,请确保将日志记录器传递给需要它的类。以下是如何创建和传递日志记录器的示例。
<?php use Itjonction\Blockcache\BladeDirective; use Illuminate\Cache\ArrayStore; use Illuminate\Cache\Repository; use Monolog\Logger; use Monolog\Handler\StreamHandler; $cache = new Repository(new ArrayStore); $logger = new Logger('blockcache'); $logger->pushHandler(new StreamHandler(storage_path('logs/blockcache.log'), Logger::DEBUG)); $bladeDirective = new BladeDirective($cache, $logger);
在此示例中,使用自定义日志记录器实例化了 BladeDirective 类,该日志记录器将日志写入 storage/logs/blockcache.log。
5. 使用 Monolog 进行测试
为了测试目的,您可以使用 Monolog 的 TestHandler 来捕获日志消息。以下是如何设置测试的示例。
<?php use Monolog\Logger; use Monolog\Handler\TestHandler; use Itjonction\Blockcache\BladeDirective; use Illuminate\Cache\ArrayStore; use Illuminate\Cache\Repository; class BladeDirectiveTest extends TestCase { protected Logger $logger; protected TestHandler $testHandler; public function setUp(): void { parent::setUp(); $this->testHandler = new TestHandler(); $this->logger = new Logger('blockcache_test'); $this->logger->pushHandler($this->testHandler); } public function test_logging_unknown_strategy() { $cache = new Repository(new ArrayStore); $directive = new BladeDirective($cache, $this->logger); $directive->setUp('test_key', ['unknown_strategy' => true]); $directive->tearDown(); $this->assertTrue($this->testHandler->hasErrorThatContains('Unknown strategy: unknown_strategy')); } }
在此测试中,使用 TestHandler 捕获并断言是否已记录正确的错误消息。
待办事项
- 链接到一个 POC 的视频。
- 设置一个标志以避免在开发中缓存。
- Stale-While-Revalidate
- 条件请求
- 事件驱动失效
- 添加组合策略的能力
- 使用中间件在模板更改时失效
- 在不使用中间件的情况下在模板更改时失效