分类 Laravel 下的文章

轻量级全文检索引擎 TNTSearch 和中文分词

之前全文检索一直用的Xunsearch, 但对于大多数中小项目来说, 用Xunsearch, Elastic总有点杀鸡用牛刀的感觉,所以想找一个轻量级的解决方案,碰巧在论坛看到一篇SummerTNTSearch介绍的一个
贴子 《TNTSearch - PHP 实现的全文索引引擎》,觉得很符合需求。

TNTSearch 的基本原理是用SQLite作为索引数据库,将要索引的字段进行分词,然后建立基于关键词的倒排索引。核心是2张表,一张存放分词后的关键词, 另一张存放关键词与文档对应的关系(相当于多对多的中间表)。

当用户输入一个查询时,将要查询的短语分词得到各个关键词,分别查询出每个关键词对应的文档ID, 再按文档中命中的关键词个数进行排序。

官方也提供了Laravel Scout的驱动 teamtnt/laravel-scout-tntsearch-driver,用起来很方便。唯一的不足就是对中文支持的不够,因为默认的分词器是以标点符号、特殊符号和空格为分隔符进行分词。

namespace TeamTNT\TNTSearch\Support;

class Tokenizer implements TokenizerInterface
{
    public function tokenize($text)
    {
        $text = mb_strtolower($text);
        return preg_split("/[^\p{L}\p{N}]+/u", $text, -1, PREG_SPLIT_NO_EMPTY);
    }
}

为了解决这个问题,我结合两个常用的php分词组件:scwsjieba-php,改写了官方的Laravel Scout驱动包, 加入了中文分词功能,发布到packagist上面。

packagist: vanry/laravel-scout-tntsearch

github: https://github.com/vanry/laravel-scout-tntsearch

有需要的同学可以去看下,readme上有使用说明,如果觉得对你有帮助,欢迎star和提issue :smiley:。

统一集中式管理你的 meta title 和 description

需求

平时在开发过程中,一般都是把标题、描述等标签直接写在模板中,如

# articles/show.blade.php

@section('title', "{$article->title} - {$article->category->name} - {$sitename}") `

在模板少的情况下问题还不大,一旦项目中有多套模板共用相同的标题和描述(比如pcmobile模板,多套不同风格主题的模板),管理起来就很繁琐了,每次修改都要先找到对应的模板,再一个一个地改。

如果能将这些标签内容抽取出来放在配置文件供复用,那就再方便不过了。

优点

  1. 再也不用人工一个一个去找对应的模板了。
  2. 用配置而不是硬编码的方式,达到一处修改,处处修改的目的。
  3. 模板中的meta标签更简洁了,再也不用字符串和变量混合在一起了。

配置文件

config目录下面新建meta.php文件

# config/meta.php
# 我喜欢用路由命名作为 key

return [
    'title' => [
        'home' => '首页标题',
        'articles.show' => '{title} - {category}',
    ],

    'description' => [
        'home' => '{sitename}为您提供...',
        'articles.show' => '{description}',
    ],
];

帮助函数

# app/helpers.php

if (! function_exists('meta')) {
    /**
     * Get meta tag from configuration.
     *
     * @param  string  $name
     * @return string
     */
    function meta($tag, $name, array $parameters = [])
    {
        $tag = config("meta.{$tag}")[$name];
       
        $search = array_map(function ($key) {
            return '{'.$key.'}';
        }, array_keys($parameters));

        $replace = array_values($parameters);

        return str_replace($search, $replace, $tag);
    }
}


if (! function_exists('meta_title')) {
    /**
     * Get meta title from configuration.
     *
     * @param  string  $name
     * @return string
     */
    function meta_title($name, array $parameters = [])
    {
        return meta('title', $name, $parameters);
    }
}

if (! function_exists('meta_description')) {
    /**
     * Get meta description from configuration.
     *
     * @param  string  $name
     * @return string
     */
    function meta_description($name, array $parameters = [])
    {
        return meta('description', $name, $parameters);
    }
}

用法

用法比较像route函数,由于是基于字符串替换,所以不能直接传对象。

# layouts/app.blade.php

<title>@yield('title') - {{ config('app.name') }}</title>


# articles/show.blade.php

@section('title', meta_title('articles.show', [
    'title' => $article->title,
    'category' => $article->category->name
]))


OR

// 命名的好处
@section('title', meta_title(Route::currentRouteName(), [
    'title' => $article->title,
    'category' => $article->category->name
]))

说明:当然了,除了标题描述标签之外,其他的标签也可以采用这种方式来统一管理。

Laravel使用二级缓存提高缓存命中率和内存使用效率

起因

一直都没找到二级缓存在php中应用的比较好的资料和案例,由于范凯RobbinWeb 应用的缓存设计模式Hibernate二级缓存的启示,记下这篇二级缓存在Eloquent ORM中的应用。

过程

比如博客的首页调用最新的20篇文章,相信不少同学在刚开始使用缓存的时候,会写下如下代码:

# 控制器
public function index()
{
    $articles = Article::latestArticles(20);

    return view('articles.index', ['articles' => $articles]);
}

# 模型
class Article extends Model
{
    public static function latestArticles($amount = 20)
    {
        return Cache::remember('articles:latest', 10, function () use ($amount) {
            return static::latest('id')->take($amount)->get();
        });
    }
}

当然,模型中还能预加载每篇文章的分类,作者和tag信息,看起来没有任何问题,而且非常符合人类直觉。但是,放大到全站缓存来看,还是有很大的改善空间。

首先,首页缓存的是一个包含20article对象的集合,集合的每一个单独的article对象除了在首页出现,还会在分类、作者和tag等列表页出现,还有文章详情页,而缓存的集合数据没办法在这些页面间共用,重复缓存大量相同的article对象是对内存资源的很大浪费,要是article中的text字段content没有单独拆分出去,内存浪费得就更严重了。

其次,不像详情页数据改动很少,首页作为列表页来说,更新频率很高,设置的缓存时间比较短,一般是分钟级别,缓存命中率并不高。

为了有效解决这两个问题,二级缓存就派上用场了,先说下自己对二级缓存的理解。

一级缓存可以看成是数据库里存的数据的一个镜像,只不过把数据从数据库搬到内存,一个key对应一条记录。key一般为表的标识符,比如keyarticles:1存的value就是id=1article对象。一级缓存时间可以设得比较长,甚至forever也行,对象修改删除时,只要删除对应的key就行。

二级缓存可以看成业务逻辑的缓存,首页最新20条文章 就属于业务逻辑,只缓存这20条文章的id,极大地节省了内存占用。等需要用到具体的数据再去一级缓存取,一级缓存没有才去查询数据库,由于都是主键查询,不会造成表的描述,查询效率非常高。即使二级缓存很快过期,一级缓存也不会失效。

个人觉得理解二级缓存最难的是要接受n+1查询这点,这个问题争议很大,明明各种ORM为了避免n+1使用了预加载,我们反而要抛弃它。包括我当初阅读范凯的《Web 应用的缓存设计模式》也心存疑惑,直到去了解了Hibernate二级缓存机制和自己在项目中的实践发现,还真是他说的那样。

拆分n+1条查询的方式,看起来似乎非常违反大家的直觉,但实际上这是真理,我实践经验证明:数据库服务器的瓶颈往往是磁盘IO,而不是SQL并发数量。因此 拆分n+1条查询本质上是以增加n条SQL语句为代价,简化复杂SQL,换取数据库服务器磁盘IO的降低 当然这样做以后,对于ORM来说,有额外的好处,就是可以高效的使用缓存了。

使用二级缓存来重构latestArticles方法


public static function latestArticles($amount = 20)
{
    // 二级缓存
    $ids = Cache::remember('articles:latest:ids', 10, function () use ($amount) {
        return static::latest('id')->take($amount)->pluck('id');
    });

    return $ids->map(function ($id) {
        // 一级缓存
        return static::findById($id); 
    });

}

public static function findById($id)
{
    return Cache::rememberForever("articles:{$id}", function () use ($id) {
        return static::find($id);
    });
}
    

除了返回Collection,还可以返回Generator

public static function latestArticles($amount = 20)
{
    // 二级缓存
    $ids = Cache::remember('articles:latest:ids', 10, function () use ($amount) {
        return static::latest('id')->take($amount)->pluck('id');
    });

    foreach ($ids as $id) {
        // 一级缓存
        yield static::findById($id); 
    }
}

更新与删除

一级缓存的更新和删除可能通过模型的updateddeleted事件来清除对应的缓存。二级缓存由于缓存时间比较短,影响不大。

关联关系

关联模型的缓存可能通过accessor来设置一个虚拟的属性来设置,比如在Article模型与Content模型是一对一的关系。
Article中:


// 一对一关联
public function content()
{
    return $this->hasOne(Content::class);
}

// contents表字段: article_id, body
public function getContentAttribute()
{
    return Cache::rememberForever("contents:{$this->id}", function () {
        return $this->content->body;
    });
}

Laravel Lumen分离Http日志和Console日志

解决命令行和fastcgi不同环境下生成的日志导致的权限问题


# Laravel: AppServiceProvider.
public function boot()
{
    if (PHP_SAPI == 'cli') {
        Log::useDailyFiles(storage_path('logs/artisan.log'), config('app.log_max_files', 5));
    }
}


# Lumen: /bootstrap/app.php
$app->configureMonologUsing(function(Monolog\Logger $monolog) {
    $filename = storage_path('logs/lumen-'.PHP_SAPI.'.log');
   
    $handler = new Monolog\Handler\RotatingFileHandler($filename, env('LOG_MAX_FILES', 5));
   
    $handler->setFormatter(new Monolog\Formatter\LineFormatter(null, null, true, true));
   
    $monolog->pushHandler($handler);
});

Laravel分页链接url伪静态

Laravel强大的路由功能使其url不管是对搜索引擎还是用户都是非常的友好,但美中不足的是它的分页默认url却是带参数查询的(http://www.lara.com/posts?page=2),这让做惯了seo的我心里总感觉有点别扭。

为了看着顺眼点,决定动手对它改造一下,首先要了解它的工作原理,打开分页器的服务提供者PaginationServiceProvider.php,查看源代码:

<?php

namespace Illuminate\Pagination;

use Illuminate\Support\ServiceProvider;

class PaginationServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        Paginator::currentPathResolver(function () {
            return $this->app['request']->url();
        });

        Paginator::currentPageResolver(function ($pageName = 'page') {
            return $this->app['request']->input($pageName);
        });
    }
}

register方法中发现,laravel分页器使用http request对象中的page参数值作为当前页码,而page的值是通过GET方式获取的,而形如http://www.lara.com/posts/page/2url中没有任何GET参数。既然不能通过静态url不能传入page参数,我们就手工注入。在路由中,使用request对象的merge方法将page参数添加进去。

Route::get('posts/page/{page?}', function ($page = 1) {
    Request::merge(['page' => $page]);
    $posts = Post::orderBy('id', 'DESC')->paginate(3);

    return view('post.index', ['posts' => $posts]);
});

阅读剩余部分