Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.github export-ignore
examples export-ignore
tests export-ignore
.gitattributes export-ignore
.gitignore export-ignore
Dockerfile export-ignore
phpunit.xml export-ignore
README.md export-ignore
11 changes: 0 additions & 11 deletions .github/dependabot.yml

This file was deleted.

6 changes: 4 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ jobs:
matrix:
laravel: [10, 11, 12, 13]
php: [8.2, 8.3, 8.4, 8.5]
scout: [10, 11]
typesense: [5, 6]
exclude:
- laravel: 13
php: 8.2

steps:
- uses: actions/checkout@v6

- name: test against Laravel ${{ matrix.laravel }} on PHP ${{ matrix.php }}
run: docker build . --build-arg PHP_VERSION=${{ matrix.php }} --build-arg LARAVEL=${{ matrix.laravel }}
- name: test against Laravel ${{ matrix.laravel }} on PHP ${{ matrix.php }} with Scout ${{ matrix.scout }} and Typesense ${{ matrix.typesense }}
run: docker build . --build-arg PHP_VERSION=${{ matrix.php }} --build-arg LARAVEL=${{ matrix.laravel }} --build-arg SCOUT=${{ matrix.scout }} --build-arg TYPESENSE=${{ matrix.typesense }}
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
vendor
composer.lock
coverage-html
coverage.xml
.phpunit.result.cache
.idea
11 changes: 4 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ FROM php:$PHP_VERSION-cli-alpine

RUN apk add git zip unzip autoconf make g++

# apparently newer xdebug needs these now?
RUN apk add --update linux-headers

RUN pecl install xdebug && docker-php-ext-enable xdebug

RUN curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer

Expand All @@ -22,8 +17,10 @@ USER dev
COPY --chown=dev composer.json ./

ARG LARAVEL=10
RUN composer require laravel/framework ^$LARAVEL.0
ARG SCOUT=10
ARG TYPESENSE=5
RUN composer require laravel/framework:^$LARAVEL.0 laravel/scout:^$SCOUT.0 typesense/typesense-php:^$TYPESENSE.0

COPY --chown=dev . .

RUN composer test
RUN composer test
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Laravel Typesense Tools

This package adds Artisan commands that help manage Typesense Collections and Collection Aliases in Laravel applications using Laravel Scout.

Using Typesense with Scout requires you to define the schema in the Scout config file so we change the expected functions to `static` to reduce duplication. This means that the functions cannot reference `app()` or `config()` because they're executed too early in the booting process.

We make use of Typesense Collection Aliases so that schema changes can be gracefully deployed to our frontends without causing issues.

See the full examples in [`examples/models/User.php`](examples/models/User.php) and [`examples/config/scout.php`](examples/config/scout.php).

## Requirements

- PHP `^8.2`
- Laravel Framework `>=10.0`
- Laravel Scout `>=10.0`
- Typesense PHP client `>=5.1`

## Installation

```bash
composer require synergitech/laravel-typesense-tools
```

## Usage

Our environments typically have separated application/frontend and worker contexts and we also usually run a separate CLI context. The deployment runbook is usually as follows.

1. deploy new code to the CLI and run any migrations
2. deploy new code to the workers with an updated TYPESENSE_VERSION_SUFFIX. We typically use a date for the suffix for easy versioning, i.e. 20260325.
3. run `php artisan search:setup` to populate the new collections
4. when the queue is empty, the new collection schema fields are now available as `search:switch-alias` is run as the final queue job
5. deploy the new code to the frontend so the new fields are in use
6. (optional) run `php artisan search:cleanup` to remove the now unused collections and get some memory back

## Command reference

- `search:setup` to ensure collections exist and (optionally) import data.
- `search:switch-alias` to point aliases at the current index collections.
- `search:delete-index {suffix}` to remove old suffixed collections.
- `search:cleanup` to remove collections that are not referenced by an alias.

### `search:setup`

Populates all known collections and dispatches a `search:switch-alias` at the end. If `--only-index` is provided, it checks each Collection/Alias exists. The check is useful for confirming your schema works and there aren't any errors.

Options:

- `--only-index`: only verify/create collections.
- `--flush`: flush each model before importing.

Examples:

```bash
php artisan search:setup --only-index
php artisan search:setup --flush
```

### `search:switch-alias`

Upserts each model's alias (`searchableAs`) to target the active collection (`indexableAs`). This is used automatically after a call to `search:setup`

```bash
php artisan search:switch-alias
```

### `search:delete-index {suffix}`

Deletes old collections named as:

- `<searchableAs>_<suffix>`

Example:

```bash
php artisan search:delete-index 20260325
```

### `search:cleanup`

Finds and optionally deletes collections not currently referenced by any alias.

```bash
php artisan search:cleanup
```
21 changes: 13 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/framework": "^10",
"laravel/framework": ">=10.0",
"laravel/scout": ">=10.0",
"typesense/typesense-php": ">=5.1",
"symfony/http-client": "^7.4"
"typesense/typesense-php": ">=5.1"
},
"require-dev": {
"larastan/larastan": "^2.0",
"orchestra/testbench": "^8.0|^9.0|^10.0|^11.0",
"larastan/larastan": "^2.0|^3.0",
"orchestra/testbench": ">=8.0",
"phpstan/extension-installer": "^1.4",
"phpunit/phpunit": "^10.0|^11.0"
"phpunit/phpunit": ">=10.0"
},
"autoload": {
"psr-4": {
Expand All @@ -33,10 +32,16 @@
"phpstan analyse --memory-limit 1G --level 8 src tests"
]
},
"extra": {
"laravel": {
"providers": [
"SynergiTech\\LaravelTypesenseTools\\LaravelTypesenseToolsServiceProvider"
]
}
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true,
"php-http/discovery": true
"phpstan/extension-installer": true
}
}
}
19 changes: 19 additions & 0 deletions examples/config/scout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

use App\Models\User;

return [
// ... Other Scout config goes here

'typesense' => [
'model-settings' => [
User::class => [
'name' => User::indexableAs(),
'collection-schema' => User::getCollectionSchema(),
'search-parameters' => [
'query_by' => implode(',', User::typesenseQueryBy()),
],
],
],
],
];
55 changes: 55 additions & 0 deletions examples/models/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace App\Models;

use Laravel\Scout\Searchable;

class User
{
use Searchable;

public static function searchableAs(): string
{
return 'users';
}

public static function indexableAs(): string
{
// @phpstan-ignore larastan.noEnvCallsOutsideOfConfig
return self::searchableAs() . '_' . env('TYPESENSE_VERSION_SUFFIX', 'default');
}

public static function getCollectionSchema(): array
{
return [
'name' => self::indexableAs(),
'fields' => [
[
'name' => 'id',
'type' => 'string',
],
[
'name' => 'name',
'type' => 'string',
],
[
'name' => 'email',
'type' => 'string',
],
[
'name' => 'created_at',
'type' => 'int64',
],
],
'default_sorting_field' => 'created_at',
];
}

public static function typesenseQueryBy(): array
{
return [
'name',
'email',
];
}
}
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
stubFiles:
- stubs/TypesenseClient.stub
- stubs/TypesenseEngine.stub
5 changes: 0 additions & 5 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@
<server name="APP_KEY" value="AckfSECXIvnK5r28GVIWUAxmbBSjTsmF"/>
<server name="DB_CONNECTION" value="testing"/>
</php>
<!-- <coverage>
<report>
<html outputDirectory="coverage-html"/>
</report>
</coverage> -->
<source>
<include>
<directory suffix=".php">./src</directory>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace SynergiTech\LaravelTypesenseTools\Commands;
namespace SynergiTech\LaravelTypesenseTools\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Scout\EngineManager;
Expand All @@ -18,11 +18,11 @@ public function handle(): int
/** @var TypesenseEngine $typesense */
$typesense = app(EngineManager::class)->driver('typesense');

/** @phpstan-ignore-next-line */
$collections = $typesense->getCollections();

/** @var array<array{name:string}> $collectionsResponse */
$collectionsResponse = $collections->retrieve();

/** @phpstan-ignore-next-line */
$collectionNames = collect($collectionsResponse['collections'] ?? $collectionsResponse)
->pluck('name')
->filter()
Expand All @@ -34,9 +34,9 @@ public function handle(): int
return Command::SUCCESS;
}

/** @phpstan-ignore-next-line */
/** @var array{aliases:array<array{collection_name:string}>}|array<array{collection_name:string}> $aliasesResponse */
$aliasesResponse = $typesense->getAliases()->retrieve();
/** @phpstan-ignore-next-line */

$aliasedCollectionNames = collect($aliasesResponse['aliases'] ?? $aliasesResponse)
->pluck('collection_name')
->filter()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace SynergiTech\LaravelTypesenseTools\Commands;
namespace SynergiTech\LaravelTypesenseTools\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Scout\EngineManager;
Expand All @@ -21,6 +21,9 @@ public function handle(): int
/** @var TypesenseEngine $typesense */
$typesense = app(EngineManager::class)->driver('typesense');

/** @var string $suffix */
$suffix = $this->argument('suffix');

foreach ($models as $model => $settings) {
$model = new $model();

Expand All @@ -30,11 +33,9 @@ public function handle(): int
}

try {
/** @phpstan-ignore-next-line */
$typesense->getCollections()->{($model->searchableAs() . '_' . $this->argument('suffix'))}->delete();
$typesense->getCollections()->{($model->searchableAs() . '_' . $suffix)}->delete();
} catch (Throwable $e) {
/** @phpstan-ignore-next-line */
$this->info('Failed to delete collection ' . $model->searchableAs() . '_' . $this->argument('suffix') . ': ' . $e->getMessage());
$this->info('Failed to delete collection ' . $model->searchableAs() . '_' . $suffix . ': ' . $e->getMessage());
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/Console/Setup.php → src/Console/Commands/Setup.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace SynergiTech\LaravelTypesenseTools\Commands;
namespace SynergiTech\LaravelTypesenseTools\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
Expand Down Expand Up @@ -59,15 +59,15 @@ private function verifyCollectionExists(string $model): void
$this->error('Please ensure the indexableAs method is implemented in ' . $model::class);
return;
}
/** @phpstan-ignore-next-line */

$index = $typesense->getCollections()->{$model->indexableAs()};

try {
$index->retrieve();
} catch (ObjectNotFound) {
$this->info('Creating collection as did not exist for ' . $model::class);
$schema = config('scout.typesense.model-settings.' . $model::class . '.collection-schema') ?? [];
/** @phpstan-ignore-next-line */

$typesense->getCollections()->create($schema);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace SynergiTech\LaravelTypesenseTools\Commands;
namespace SynergiTech\LaravelTypesenseTools\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Scout\EngineManager;
Expand Down Expand Up @@ -29,7 +29,6 @@ public function handle(): int
}

try {
/** @phpstan-ignore-next-line */
$typesense->getAliases()->upsert($model->searchableAs(), [
'collection_name' => $model->indexableAs(),
]);
Expand Down
Loading