maratzhe / symfony-typesense
Integrate Typesense to Symfony & Doctrine ORM
Installs: 4
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/maratzhe/symfony-typesense
Requires
- php: ^8.3
- doctrine/orm: ^3.5
- symfony/http-client: ^7.3
- typesense/typesense-php: ^5.2
Requires (Dev)
- doctrine/doctrine-bundle: ^2.4
- doctrine/doctrine-migrations-bundle: ^3.7
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.5
- symfony/framework-bundle: ^7.3
- symfony/maker-bundle: ^1.65
- symfony/runtime: ^7.3
- symfony/uid: ^7.3
- symfony/yaml: ^7.3
- zenstruck/foundry: ^2.8
README
A Typesense integration for the Symfony web framework.
Attributes based and completely auto mapping.
Features:
- Attributes based
- Completely auto mapping based on Doctrine types. Manual mapping also available.
- Relations management. Auto update parent entity when child relation was updated.
Installation
Install the bundle using composer:
composer require acseo/typesense-bundle
<?php // config/bundles.php return [ Maratzhe\SymfonyTypesense\Bundle\TypesenseBundle::class => ['all' => true], ]
Configure the Bundle:
# .env TYPESENSE_DSN=http://localhost:8108?api_key=xyz
Docker
If you use Symfony with docker image add to compose.yaml:
typesense: image: typesense/typesense:27.1 restart: on-failure ports: - "8108:8108" volumes: - typesense_data:/data command: '--data-dir /data --api-key=xyz --enable-cors'
And edit .env file:
# .env TYPESENSE_DSN=http://typesense:8108?api_key=xyz
Mapping
#[SearchCollection]
use Maratzhe\SymfonyTypesense\Enum\SyncMode; use Maratzhe\SymfonyTypesense\Attribute\SearchCollection; #[Entity] #[SearchCollection(name: 'product', sync: SyncMode::AUTO)] class Product
Parameters:
- name: string. Collection name in Typesense. Default: class name.
- sync: SyncMode. Update mode. SyncMode::AUTO - update collection on entity create, update or remove. SyncMode::NONE - update entity only by CLI command. Default: SyncMode::NONE.
#[SearchField]
use Maratzhe\SymfonyTypesense\Attribute\SearchField; use Maratzhe\SymfonyTypesense\Enum\FieldType; ... #[Column(type: 'string', length: '2048')] #[SearchField] public string $description = ''; ...
Parameters:
- name: field name in Typesense. Default: entity field name.
- type: FieldType. Field type in Typesense. Default: null (mapping from Doctrine ORM type).
- locale: string. Field locale. Default: "".
- optional: bool. Optional field. Default: true.
- facet: bool. Enables faceting on the field. Default: false.
- index: bool. Enables index on the field. Default: false.
- infix: bool. Enables infix search on the field. Default: false.
- sort: bool. Enables sort on the field. Default: false.
- stem: bool. Enables stem on the field. Default: false.
#[SearchRelation]
use Maratzhe\SymfonyTypesense\Attribute\SearchCollection; ... #[OneToMany(targetEntity: Composition::class, mappedBy: 'product', cascade: ['all'], orphanRemoval: true)] #[SearchRelation(sync: SyncMode::AUTO, bulk: true)] public Collection $compositions; ...
Parameters:
- sync: SyncMode. Update mode. SyncMode::AUTO - update entity on relation create, update or remove. SyncMode::NONE - disable auto update. Default: SyncMode::NONE.
- bulk: bool. On true - update relation on CLI search:import command. Default: false.
Search
use Maratzhe\SymfonyTypesense\Factory\FinderFactory; final class ProductController extends AbstractController { #[Route('/product', name: 'product.index')] public function index(FinderFactory $factory): Response { $finder = $factory->create(Product::class); $result = $finder->query('blue', 'description') ->filterBy('published := true') ->sortBy('id:desc') ->getResult(); //dump($result); //\Maratzhe\SymfonyTypesense\Search\Result { // +facet_counts: [] // +found: 1 // +page: 1 // +pages: 1 // +per_page: 10 // +out_of: 110 // +search_time_ms: 0 // +hits: array:1 [ // 0 => Maratzhe\SymfonyTypesense\Search\Hit { // +document: App\Entity\Product { ▶} // +highlight: array:1 [ ▶] // +text_match: 578730123365187705 // +text_match_info: array:7 [ ▶] // } // ] //} } }
CLI commands
search:create {index}
Create Typesense collection.
Arguments:
- index: index name.
Options:
- all: create indexes for all entities.
search:import {index}
Import collection from database to Typesense.
Arguments:
- index: index name.
Options:
- all: import all collections.
- first-page: page to start population from. Default: 1
- last-page: page to end population on. Default: null
- per-page: entities per page. Default: 1000
search:mapping {index}
Show collection mapping.
Arguments:
- index: index name.
Options:
- all: show mapping for all collections.
- real: show mapping from Typesense instead of generated.
search:show {index} {id}
Show document from Typesense.
Arguments:
- index: index name.
- id: document ID.
Example
#[Entity]
#[SearchCollection(name: 'product', sync: SyncMode::AUTO)]
class Product
{
#[Id]
#[Column]
#[GeneratedValue]
//ID of entity mapping by default
public ?int $id = null;
#[Column(type: 'custom_id', nullable: true)]
#[SearchField]
//Use Doctrine mapping custom type
public ?CustomId $custom_id;
/** @var array<int, Color> */
#[Column(type: 'color_array')]
#[SearchField(name: 'colors', type: FieldType::INT32_ARRAY, facet: true, index: true)]
public array $colors;
/** @var array<int, Photo> */
#[Column(type: 'photo_array')]
#[SearchField(name: 'photos.*', type: FieldType::OBJECT_ARRAY)]
#[SearchField(name: 'photos.*.size', type: FieldType::INT32)]
#[SearchField(name: 'photos.*.url', type: FieldType::STRING)]
//Complex manual mapping for array of objects
public array $photos;
#[Column(nullable: true)]
#[SearchField(facet:true, index: true)]
public ?Pattern $pattern;
#[Embedded(class: Price::class)]
//for embedded objects mapping must be in embedded class.
public ?Price $price;
/** @var Collection<int, Composition> $compositions */
#[OneToMany(targetEntity: Composition::class, mappedBy: 'product', cascade: ['all'], orphanRemoval: true)]
#[SearchRelation(sync: SyncMode::AUTO, bulk: true)]
//When child relation was updated this entity will update too. Also all relations will be updated by calling "search:import"
public Collection $compositions;
#[OneToOne(targetEntity: Properties::class, cascade: ['all'], orphanRemoval: true)]
#[SearchRelation(bulk: true)]
//No update after child relation updated. Update only by CLI command.
public ?Properties $properties;
#[Column]
#[SearchField(index: true)]
public bool $published;
#[Column]
#[SearchField(index: true)]
public string $description;
public function __construct(
?CustomId $custom_id = null,
array $colors = [],
array $photos = [],
?Pattern $pattern = null,
?Price $price = null,
array $compositions = [],
?Properties $properties = null,
bool $published = false,
string $description = ''
)
{
$this->custom_id = $custom_id;
$this->colors = $colors;
$this->photos = $photos;
$this->compositions = new ArrayCollection($compositions);
$this->pattern = $pattern;
$this->price = $price;
$this->properties = $properties;
$this->published = $published;
$this->description = $description;
foreach ($this->compositions as $composition) {
$composition->product = $this;
}
}
}
Mapping for this class:
➜ :./bin/console search:mapping product Mapping of product (class: App\Entity\Product), generated ╔══════════════════════════════╤══════════╤════════╤══════════╤═══════╤═══════╤═══════╤══════╤══════╗ ║ name │ type │ locale │ optional │ facet │ index │ infix │ sort │ stem ║ ╠══════════════════════════════╪══════════╪════════╪══════════╪═══════╪═══════╪═══════╪══════╪══════╣ ║ id │ int32 │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ custom_id │ string │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ colors │ int32[] │ │ yes │ yes │ yes │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ photos.* │ object[] │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ photos.*.size │ int32 │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ photos.*.url │ string │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ pattern │ string │ │ yes │ yes │ yes │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ price.price │ int32 │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ price.currency │ string │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ published │ bool │ │ yes │ no │ yes │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ description │ string │ │ yes │ no │ yes │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ compositions.* │ object[] │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ compositions.*.id │ int32 │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ compositions.*.value │ int32 │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ compositions.*.material │ object │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ compositions.*.material.id │ int32 │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ compositions.*.material.name │ string │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ properties │ object │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ properties.id │ int32 │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ properties.name │ string │ │ yes │ no │ no │ no │ no │ no ║ ╟──────────────────────────────┼──────────┼────────┼──────────┼───────┼───────┼───────┼──────┼──────╢ ║ properties.value │ string │ │ yes │ no │ no │ no │ no │ no ║ ╚══════════════════════════════╧══════════╧════════╧══════════╧═══════╧═══════╧═══════╧══════╧══════╝