Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Custom Index Bundle Laravel Package

intaro/custom-index-bundle

Symfony bundle for adding and managing custom Doctrine database indexes via configuration, so you can tune query performance without modifying entity mappings. Useful for optimizing large tables and controlling indexes from application config.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. intaro/custom-index-bundle is a Symfony bundle built on Doctrine ORM, not a Laravel package. composer require intaro/custom-index-bundle in an Eloquent app pulls in Symfony/Doctrine dependencies and does nothing — there is no service provider, no config file, no schema hook Laravel understands. With 27 stars, zero dependents, and a single maintainer, treat it as a reference implementation of a pattern, not a dependency you adopt.

What v1.1.0 changes (and doesn't). The release is maintenance, not features: Docker dev env, php-cs-fixer, PHPStan, CI, assorted fixes, and — the only item that matters to you — doctrine/orm 3 compatibility. There is still no Laravel integration, no service provider, no new index capability. Two practical consequences:

  • If you're in the Doctrine-in-Laravel scenario and on ORM 3, you now need ^1.1; on ORM 2, 1.0.3 remains fine and 1.1.0 should also work.
  • The new CI/PHPStan/Docker setup means you can actually run the bundle's test suite locally and read the SQL it asserts on — a faster way to extract the DDL you want than reverse-engineering the generator by eye.

There are only two scenarios where the code itself is useful to you:

  1. You're running Doctrine inside Laravel (via a bridge such as mitchellvanw/laravel-doctrine or a custom integration). Then you can register the bundle's event subscriber manually and get its attribute DSL for free — and as of 1.1.0, on ORM 3.
  2. You're on Eloquent (almost certainly). Then you want the ~200 lines that turn an index definition into platform-specific DDL — copy those into raw migrations and move on.

Where to look first, in this order:

  • The annotation/attribute class (src/Annotations/CustomIndex.php or src/Attribute/...). This is the entire public DSL: index name, columns, unique, using (btree/gin/gist/brin/hash), and where for partial indexes. Unchanged in 1.1.0 — the API surface is stable, which is good news for anyone vendoring it.
  • The Doctrine event subscriber that hooks postGenerateSchema / onSchemaCreateTable. This is where the bundle decides when to emit DDL — and it's the piece you'd reimplement as a migration. Check this file specifically when upgrading to 1.1.0: ORM 3 reworked schema/event APIs, so if you have a custom integration wired against the old signature, that's your breakage point.
  • The per-platform SQL generator. This is the part you actually want to read: it shows exactly how each index type is spelled on PostgreSQL vs MySQL, and where it bails out.

Your first use case should be the one that made you search for this package: a partial index, e.g. CREATE INDEX orders_pending_idx ON orders (created_at) WHERE status = 'pending', or a GIN index on a jsonb column. Write the equivalent DB::statement() in a Laravel migration, then diff it against what the bundle generates for the same definition — easiest now by cloning the repo, using the new Docker env, and running the platform generator tests. If they match, you've extracted the value and can uninstall it.

Implementation Patterns

The attribute-on-entity pattern (Doctrine only). The bundle's model is class-level metadata:

#[CustomIndex(name: 'users_email_lower_idx', columns: ['lower(email)'], unique: true)]
class User { ... }

Then doctrine:migrations:diff picks it up and emits the DDL. The key architectural idea to steal: index definitions live next to the model, but DDL is generated by a migration tool, not at runtime. Nothing happens at application boot. That's a good property — replicate it.

The Eloquent equivalent — a dedicated migration per index type. Keep custom indexes out of $table->index() (which can't express expressions, WHERE, USING, or INCLUDE) and out of your create_* migrations (which should stay portable). One migration, one DB::statement, guarded by driver:

public function up(): void
{
    if (DB::getDriverName() !== 'pgsql') return;
    DB::statement("CREATE INDEX CONCURRENTLY orders_pending_idx
                   ON orders (created_at) WHERE status = 'pending'");
}

Pattern catalog worth adopting:

  • Partial index for soft deletes / status filters. Pairs naturally with Eloquent's SoftDeletes global scope, because the scope emits deleted_at IS NULL on every query — which is exactly the predicate the planner needs to use the index. This is the highest-value pattern for a typical Laravel app.
  • GIN on jsonb for whereJsonContains / -> operator queries. Match the index's opclass to the operators you actually use (jsonb_path_ops is smaller but only supports @>).
  • Expression index on lower(email) to enforce case-insensitive uniqueness without a citext extension. Remember the query must also be whereRaw('lower(email) = ?') — where('email', ...) won't use it.
  • BRIN on created_at for append-only, time-ordered tables (logs, events) where a btree would be huge.
  • Covering index (INCLUDE) for hot list endpoints where you want an index-only scan.

Integration tips:

  • Set public $withinTransaction = false; on the migration when using CREATE INDEX CONCURRENTLY — Laravel wraps migrations in a transaction by default and Postgres will reject it.
  • Name indexes explicitly and deterministically. Laravel's auto-generated names truncate and hash inconsistently across drivers; you need the exact name in down().
  • Add a schema assertion test: query pg_indexes for the index name in CI. It's the only way to catch a migration that silently no-ops because of a driver check.
  • If you're porting the Doctrine subscriber: hook the equivalent of "after schema build, before diff" and register the platform generator per driver, so adding MySQL support later doesn't require touching the definition parsing.
  • If you're vendoring rather than depending: pin to a specific commit or tag. 1.1.0 includes "fixes by php-cs-fixer and phpstan" — those can be behaviour changes in edge cases (null handling, array shapes, strict comparisons in the generator). Diff generated SQL for your index definitions against 1.0.3 before you swap.

Gotchas and Tips

  • Everything interesting is PostgreSQL-only. USING gin|gist|brin, WHERE, INCLUDE, and expression columns have no MySQL equivalent (MySQL 8 has functional indexes but with different syntax and no partial indexes at all). The bundle's per-platform generator exists precisely to skip or fail on unsupported drivers — expect the same in your port, and be explicit about whether "unsupported" means silent skip or exception. Silent skip is how indexes go missing in production.
  • Expression indexes are invisible to the schema builder. $table->index('lower(email)') produces a broken identifier. Raw SQL only, and the down() must be raw too.
  • DROP INDEX on Postgres takes just the name — which is why deterministic naming matters. On MySQL you need ON table. Don't share a down() between drivers.
  • Duplicate indexes are a real cost. If you add #[ORM\Index] or $table->index(['status','created_at']) and a custom partial index on the same columns, both get maintained on every write. Audit pg_indexes for overlapping column lists before shipping.
  • 63-byte identifier limit in Postgres truncates silently. A truncated name is one your down() can never find, and one your schema-assertion test will fail on mysteriously.
  • CREATE INDEX CONCURRENTLY cannot run inside a transaction, cannot run on a table with a pending schema change, and can leave an INVALID index behind on failure. After any concurrent index build, check pg_index.indisvalid and REINDEX if needed.
  • Upgrading to 1.1.0 — what to actually check. The ORM 3 compatibility PR touches metadata/attribute and event-subscriber internals. If you have a Doctrine-in-Laravel integration, verify the subscriber still registers and that doctrine:schema:update --dump-sql still emits your custom indexes. If you vendored the source, git diff v1.0.3..v1.1.0 -- src/ and focus on the platform generator and annotation reader — the php-cs/phpstan pass may have altered typing or null-coalescing in the SQL-building path.
  • Debugging Doctrine path: if you are in the Symfony/Doctrine scenario, doctrine:schema:update --dump-sql is the fastest way to see what the bundle actually intends to emit. In Laravel, DB::listen() or php artisan migrate --pretend (which does show raw statements, but won't execute them).
  • Extension points if you fork: the annotation reader and the platform SQL generator are the two seams. The platform class is the only thing you'd need to subclass to target a new database. Given the API surface is small and stable across 1.0 → 1.1, vendoring the 2–3 relevant files into app/Support/ is more defensible than depending on a 27-star package with no dependents and a single maintainer — you get the pattern, the SQL generation logic, and zero upgrade risk. Pin the version if you do depend on it, and assume you'll be maintaining the fork. The new CI/PHPStan setup modestly improves the odds the package stays healthy, but one contributor plus one release in a year is still a signal, not a guarantee.
  • Last note on the concept vs. the code: the bundle solves a Doctrine-specific problem — Doctrine's schema tool doesn't introspect non-standard indexes, so it tries to drop them on every diff. Eloquent has no schema introspection step, so that problem never arises. You get the benefit of partial/expression indexes with none of the drift, at the cost of writing raw SQL. That trade is almost always worth it — and nothing in 1.1.0 changes it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator