本指南将引导您在多租户 Laravel 应用程序中实现搜索功能。我们将使用客户关系管理 (CRM) 应用程序作为示例,该应用程序允许用户存储联系人。

要求

本指南需要:
  • 一个配置了 Laravel Scout 使用 meilisearch 驱动程序的 Laravel 10 应用程序
  • 一个正在运行的 Meilisearch 服务器 — 请参阅我们的快速入门
  • 一个搜索 API 密钥 — 可在您的 Meilisearch 控制面板中找到
  • 一个搜索 API 密钥 UID — 使用密钥端点检索
更喜欢自托管?阅读我们的安装指南

模型与关系

我们的示例 CRM 是一个多租户应用程序,其中每个用户只能访问属于其组织的数据。 在技术层面,这意味着:
  • 一个属于 OrganizationUser 模型
  • 一个属于 OrganizationContact 模型(只能由同一组织的用户访问)
  • 一个拥有许多 User 和许多 ContactOrganization 模型
考虑到这一点,第一步是定义这些模型及其关系: app/Models/Contact.php 中:
<?php

namespace App\Models;

use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Contact extends Model
{
    use Searchable;

    public function organization(): BelongsTo
    {
        return $this->belongsTo(Organization::class, 'organization_id');
    }
}
app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    public function organization()
    {
        return $this->belongsTo(Organization::class, 'organization_id');
    }
}
app/Models/Organization.php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Organization extends Model
{
    public function contacts(): HasMany
    {
        return $this->hasMany(Contact::class);
    }
}
现在您对应用程序的模型及其关系有了深入的了解,您可以生成租户令牌了。

生成租户令牌

目前,所有 User 都可以搜索所有 Organization 的数据。为了防止这种情况发生,您需要为每个组织生成一个租户令牌。然后,您可以使用此令牌向 Meilisearch 进行身份验证请求,并确保用户只能访问其组织的数据。同一 Organization 内的所有 User 将共享相同的令牌。 在本指南中,您将在从数据库检索组织时生成令牌。如果组织没有令牌,您将生成一个并将其存储在 meilisearch_token 属性中。 更新 app/Models/Organization.php
<?php

namespace App\Models;

use DateTime;
use Laravel\Scout\EngineManager;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Log;

class Organization extends Model
{

    public function contacts(): HasMany
    {
        return $this->hasMany(Contact::class);
    }

    protected static function booted()
    {
        static::retrieved(function (Organization $organization) {
            // You may want to add some logic to skip generating tokens in certain environments
            if (env('SCOUT_DRIVER') === 'array' && env('APP_ENV') === 'testing') {
                $organization->meilisearch_token = 'fake-tenant-token';
                return;
            }

            // Early return if the organization already has a token
            if ($organization->meilisearch_token) {
                Log::debug('Organization ' . $organization->id . ': already has a token');
                return;
            }
            Log::debug('Generating tenant token for organization ID: ' . $organization->id);

            // The object belows is used to generate a tenant token that:
            // • applies to all indexes
            // • filters only documents where `organization_id` is equal to this org ID
            $searchRules = (object) [
                '*' => (object) [
                    'filter' => 'organization_id = ' . $organization->id,
                ]
            ];

            // Replace with your own Search API key and API key UID
            $meiliApiKey = env('MEILISEARCH_SEARCH_KEY');
            $meiliApiKeyUid = env('MEILISEARCH_SEARCH_KEY_UID');

            // Generate the token
            $token = self::generateMeiliTenantToken($meiliApiKeyUid, $searchRules, $meiliApiKey);

            // Save the token in the database
            $organization->meilisearch_token = $token;
            $organization->save();
        });
    }

    protected static function generateMeiliTenantToken($meiliApiKeyUid, $searchRules, $meiliApiKey)
    {
        $meilisearch = resolve(EngineManager::class)->engine();

        return $meilisearch->generateTenantToken(
            $meiliApiKeyUid,
            $searchRules,
            [
                'apiKey' => $meiliApiKey,
                'expiresAt' => new DateTime('2030-12-31'),
            ]
        );
    }
}
现在 Organization 模型正在生成租户令牌,您需要将这些令牌提供给前端,以便它能够安全地访问 Meilisearch。

在 Laravel Blade 中使用租户令牌

使用 视图组合器 为视图提供搜索令牌。这样,您可以确保令牌在所有视图中都可用,而无需手动传递。
如果您愿意,可以使用 with 方法手动将令牌传递给每个视图。
创建一个新的 app/View/Composers/AuthComposer.php 文件
<?php

namespace App\View\Composers;

use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Vite;
use Illuminate\View\View;

class AuthComposer
{
    /**
     * Create a new profile composer.
     */
    public function __construct() {}

    /**
     * Bind data to the view.
     */
    public function compose(View $view): void
    {
        $user = Auth::user();
        $view->with([
            'meilisearchToken' => $user->organization->meilisearch_token,
        ]);
    }
}
现在,在 AppServiceProvider 中注册此视图组合器
<?php

namespace App\Providers;

use App\View\Composers\AuthComposer;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        // Use this view composer in all views
        View::composer('*', AuthComposer::class);
    }
}
瞧!所有视图现在都可以访问 meilisearchToken 变量。您可以在前端使用此变量。

构建搜索 UI

本指南使用 Vue InstantSearch 构建您的搜索界面。Vue InstantSearch 是一组组件和辅助工具,用于在 Vue 应用程序中构建搜索 UI。如果您喜欢其他 JavaScript 风格,请查看我们的其他前端集成 首先,安装依赖项:
npm install vue-instantsearch @meilisearch/instant-meilisearch
现在,创建一个使用 Vue InstantSearch 的 Vue 应用程序。打开一个新的 resources/js/vue-app.js 文件
import { createApp } from 'vue'
import InstantSearch from 'vue-instantsearch/vue3/es'
import Meilisearch from './components/Meilisearch.vue'

const app = createApp({
  components: {
    Meilisearch
  }
})

app.use(InstantSearch)
app.mount('#vue-app')
此文件初始化您的 Vue 应用程序并将其配置为使用 Vue InstantSearch。它还注册了您接下来将创建的 Meilisearch 组件。 Meilisearch 组件负责初始化 Vue Instantsearch 客户端。它使用 @meilisearch/instant-meilisearch 包来创建与 Instantsearch 兼容的搜索客户端。 resources/js/components/Meilisearch.vue 中创建它:
<script setup lang="ts">
import { instantMeiliSearch } from "@meilisearch/instant-meilisearch"

const props = defineProps<{
  host: string,
  apiKey: string,
  indexName: string,
}>()

const { searchClient } = instantMeiliSearch(props.host, props.apiKey)
</script>

<template>
  <ais-instant-search :search-client="searchClient" :index-name="props.indexName">
    <!-- Slots allow you to render content inside this component, e.g. search results -->
    <slot name="default"></slot>
  </ais-instant-search>
</template>
您可以在任何 Blade 视图中使用 Meilisearch 组件,并通过租户令牌提供它。不要忘记添加 @vite 指令,将 Vue 应用程序包含在您的视图中。
<!-- resources/views/contacts/index.blade.php -->

<div id="vue-app">
    <meilisearch index-name="contacts" api-key="{{ $meilisearchToken }}" host="https://edge.meilisearch.com">
    </meilisearch>
</div>

@push('scripts')
    @vite('resources/js/vue-app.js')
@endpush
大功告成!您现在拥有一个安全且多租户的搜索界面。用户只能访问其组织的数据,您可以确信其他租户的数据是安全的。

结论

在本指南中,您了解了如何在 Laravel 应用程序中实现安全的多租户搜索。然后,您为每个组织生成了租户令牌,并使用它们来保护对 Meilisearch 的访问。您还使用 Vue InstantSearch 构建了一个搜索界面,并为其提供了租户令牌。 本指南中的所有代码都是我们在 Laravel CRM 示例应用程序中实现的一个简化示例。在 GitHub 上查找完整代码。
© . This site is unofficial and not affiliated with Meilisearch.