glitchr / typesense-bundle
Typesense for PHP the way Doctrine is for SQL: connections (DBAL), collections described by attributes or configuration, entities turned into documents and back, finders, queries and responses (ORM), and the indexing rule on Doctrine's events - framework-free; and its Symfony bundle.
Package info
github.com/glitchr-studio/typesense-bundle
Type:symfony-bundle
pkg:composer/glitchr/typesense-bundle
Requires
- php: ^8.0
- beberlei/doctrineextensions: ^1.0
- doctrine/doctrine-bundle: ^2.0
- doctrine/orm: ^2.0
- monolog/monolog: ^2.3
- php-http/curl-client: ^2.2
- symfony/framework-bundle: ^3.4|^4.3|^5|^6.0
- symfony/http-client-contracts: ^1.0|^2.0|^3.0
- symfony/property-access: ^3.4|^4.3|^5|^6.0
- typesense/typesense-php: ^4.8
Requires (Dev)
- phpunit/phpunit: ^9.5
- symfony/phpunit-bridge: ^5.0|^6.0
- symfony/yaml: ^3.4 || ^4.4 || ^5.4 || ^6.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Typesense for PHP, structured like Doctrine's own DBAL/ORM split - in plain PHP first, with a Symfony bundle:
DBAL/- connections, the client made from their parameters, multi-server.Attribute/-#[Collection]and#[Field]: an entity declares what is indexed, and implements nothing.ORM/Mapping/- collections, metadata (from the attributes or the configuration, completed by Doctrine'sClassMetadata: identifier, single-table-inheritance sub-collections).ORM/Query/- a fluentRequest/Querybuilder andResponsewrapper.ORM/Transformer/- entity -> document (PropertyReader: property paths, collections flattened) and the finders' document -> entity.EventListener/TypesenseIndexer- the indexing rule on Doctrine's events, on anyEventManager.Bridge/Symfony/- the bundle's configuration, services, the Doctrine listener's wiring and thetypesense:*commands (TypesenseBundleitself stays at the root of the resources).
Requires Doctrine ORM, the Typesense client and two standalone Symfony
components (Cache, PropertyAccess); symfony/framework-bundle,
doctrine/doctrine-bundle and symfony/console only in a Symfony
application. Nothing of the omni* packages is needed.
#[Collection('article', defaultSortingField: 'publishedAt')] #[Field('title', type: 'string', infix: true)] #[Field('tags', type: 'string[]', property: 'tags.slug', facet: true)] class Article { /* Doctrine mapping as usual */ } $response = $finder->query((new Query('title', 'fuj'))->infix(Query::INFIX_ALWAYS)->facetBy('tags')); $response->getResults(); // Article entities, in the order of the hits
docs/plain-php.md: the whole thing by hand, without a framework.docs/getting-started.md: in a Symfony application, including a multi-server connection setup.
Verified, and not
- Verified by the suite (
docker compose -f docker-compose.test.yml run --rm test, a real Typesense 27.1 server): plain PHP on Doctrine and SQLite with no class of a framework loaded (Integration/BareTest: indexed on flush, searched, entities hydrated in the hits' order, faceted, infix, removed); a Symfony application with FrameworkBundle, DoctrineBundle and this bundle only (Integration/SymfonyApplicationTest: a collection from the attributes and one from the configuration side by side,typesense:create, the listener, a finder,typesense:list); the attributes give the very mapping and schema the configuration does (Unit/Attribute). - Verified on an application: glitchr's lesringostars (Typesense 30.0) runs on it, four of its collections declared through omniscient/omnisearch (whose omnisearch/typesense builds this package's metadata from its attributes) and one in its configuration: the schemas it would create are those its server holds, its 628 documents and its blog's searches unchanged.
- Not verified: Typesense's multi-node clusters; PostgreSQL (the order of
the hydrated hits no longer depends on MySQL's
FIELD(), but only SQLite and MySQL were run); doctrine/orm 2.x (the constraint allows it, 3.7 is what runs).
Sharing a server: collection_prefix
A collection is named after its mapping key (mappings.article → the
article collection). Two environments of the same application pointed at
the same Typesense server — dev and test on one machine, a demo next to
them — would therefore read and write the very same collection.
collection_prefix puts a string in front of every collection name sent to
the server:
# config/packages/typesense.yaml typesense: mappings: article: class: 'App\Entity\Article' # ... when@test: typesense: collection_prefix: 'test_' # the "test_article" collection
- It is empty by default: without it, names are exactly what they were.
- It applies to everything the bundle sends to the server — creating and
deleting a collection, indexing (the Doctrine listener,
typesense:action,typesense:update), searching through a finder, thecollectionof eachmultiSearch()request — and to the sub-collections of a discriminator map (test_article__blog). - It does not change what the application sees: the mapping key stays
article, so dotypesense.finder.article,TypesenseManager::getFinder('article')/getCollection('article')andTypesenseMetadata::getName(). The name on the server isTypesenseMetadata::getCollectionName(). Connection::getCollection()/getDocuments()/getDocument()are the raw client: they take a name as it is on the server, prefix included.- Setting or changing it on an existing application points the mappings at
collections that do not exist yet: run
typesense:createandtypesense:action upsertonce. The collections under the old names are left where they are.
typesense:create only touches its own collections
bin/console typesense:create drops and recreates the collections of the
configured mappings (under their collection_prefix), and nothing else.
The server is never asked what else it holds: another application's
collections, another environment's, one created through the client
directly are left alone. There is no option to sweep the whole server.
Upgrading: typesense:create used to empty the server
Before October 2026 (2.0 branch), typesense:create started by deleting
every collection found on each configured connection, mapped or not.
If nothing but this application's mappings lives on the server, nothing
changes for you: the same collections are dropped and recreated. Two cases
do differ:
- You relied on it to wipe the server (a reset script, a CI job): delete
the collections yourself (Typesense's
DELETE /collections/<name>), or start the server on an empty data directory. - A mapping was renamed or removed, or a discriminator value dropped: the collection it used to have is no longer swept away as a side effect, and stays on the server until it is deleted by hand.
The other commands were already scoped to the mappings
(typesense:action, typesense:update) or read-only (typesense:list,
typesense:health). typesense:list shows every collection of the server
under its real name; its --collection filter takes that name or a mapping
key.
Running the tests
Two suites:
tests/Unit— no external dependencies, pure PHP + mocks. Runs anywhere PHP + this bundle's dependencies are installed.tests/Integration— exercises the realtypesense/typesense-phpclient against a real Typesense server (skips itself if none is reachable).
Locally, against the bundle's own composer install
composer install composer test-unit # fast, no server needed composer test-integration # needs TYPESENSE_URL / TYPESENSE_KEY pointing at a real server composer test # both suites composer test-coverage # HTML + text coverage report in var/coverage
Full pipeline via Docker (spins up a real Typesense server too)
docker compose -f docker-compose.test.yml run --rm test
This builds Dockerfile.test (a bare php:8.4-cli image, composer install
as its own root package — no host Symfony app needed) and starts a real,
ephemeral (tmpfs) typesense/typesense server alongside it, wired via
TYPESENSE_URL/TYPESENSE_KEY. Use this exact setup as the base for a
.gitlab-ci.yml/GitHub Actions job.
The bundle needs nothing from glitchr/omnibase: the suite is green on its own,
on a bare image without bcmath. The image resolves composer.json afresh
(.dockerignore leaves out a local vendor/ and composer.lock), as a fresh
clone would.
Inside a host application (dev workflow)
If this bundle is installed as a dependency (vendor/glitchr/typesense-bundle),
tests/bootstrap.php self-registers its test namespace against the host
app's own autoloader (which never picks up a dependency's autoload-dev):
docker exec -w /path/to/app/vendor/glitchr/typesense-bundle <web-container> \ php ../../symfony/phpunit-bridge/bin/simple-phpunit