itjonction/blockcache

适用于Laravel和纯PHP的块级缓存

1.0.4 2024-08-07 15:58 UTC

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模型扩展的父类上使用此特质。

现在,此片段的缓存键将包含对象的idupdated_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字段准确,您可以使用数据库触发器。以下是一个简单的方法:

  1. 创建数据库触发器:编写一个触发器,在每个更新操作中更新updated_at字段。这确保了字段始终被更新,无论更新来自何处。

  2. MySQL触发器示例:如果您使用MySQL,以下是一个基本示例

    DELIMITER //
    
    CREATE TRIGGER update_timestamp
    BEFORE UPDATE ON your_table_name
    FOR EACH ROW
    BEGIN
      SET NEW.updated_at = NOW();
    END//
    
    DELIMITER ;
  3. 添加Eloquent配置:确保您的模型正确使用updated_atcreated_at字段。默认情况下,Eloquent期望这些字段。

    class YourModel extends Model
    {
        public $timestamps = true;
    }
  4. 更新旧代码:逐步重构您的旧代码,使其在可能的情况下使用Eloquent进行数据库操作。

  5. 这将使管理和维护时间戳更容易。

  6. 手动更新:对于无法立即重构的应用程序部分,确保在SQL查询中手动更新updated_at字段。

    UPDATE your_table_name SET column1 = value1, updated_at = NOW() WHERE condition;

通过使用数据库触发器和逐步重构旧代码,您可以确保updated_at字段保持准确和一致。

触控

为了使此技术正常工作,我们需要一种机制来在每次更新模型时通知父级关系(随后清除父级缓存)。以下是基本工作流程:

  1. 模型在数据库中更新。
  2. 它的updated_at时间戳被刷新,触发了实例的新缓存键。
  3. 模型“触控”(或ping)其父级。
  4. 父级的updated_at时间戳被更新,清除其相关缓存。
  5. 只有受影响的片段重新渲染。所有其他缓存项保持不变。

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');

如果您尝试使用单个标签或不同的组合来检索它,它将找不到该项。

测试缓存标签

  1. 通过测试:这是因为您检查了具有确切标签组合的键的存在。

    $this->assertTrue($this->cacheManager->has('my-unique-key',['orders','invoices']));
  2. 失败测试:这是因为您使用单个标签进行检查,这与复合键不匹配。

    $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失效,它也会使带有ordersinvoices的项失效。

代码示例:

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 捕获并断言是否已记录正确的错误消息。

待办事项

  1. 链接到一个 POC 的视频。
  2. 设置一个标志以避免在开发中缓存。
  3. Stale-While-Revalidate
  4. 条件请求
  5. 事件驱动失效
  6. 添加组合策略的能力
  7. 使用中间件在模板更改时失效
  8. 在不使用中间件的情况下在模板更改时失效