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

Spine Trigonometric Dql Bundle Laravel Package

spine/spine-trigonometric-dql-bundle

Spine Trigonometric DQL Bundle adds trigonometric functions to Doctrine Query Language, letting you use sine, cosine, tangent, and related math operations directly in DQL. Keep calculations database-side and queries cleaner with simple integration into Spine/Doctrine projects.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package, so Laravel does not auto-discover it. You don't enable the Bundle class — you only need the FunctionNode subclasses it ships, which you register as Doctrine DQL functions yourself.

  1. Install via Composer. Because the package name is spine/spine-trigonometric-dql-bundle, require it directly:

    composer require spine/spine-trigonometric-dql-bundle
    

    If it pulls in Symfony framework components you already have via laravel-doctrine/orm, Composer will dedupe; if it fights with your Symfony version constraint, vendor the FunctionNode classes instead — they're pure Doctrine code with no Symfony runtime coupling.

  2. Look first at the bundle's DependencyInjection/ folder. That's where the class aliases are actually registered on the Doctrine configuration in Symfony. In Laravel you reproduce the same list under custom_dql_functions in config/doctrine.php (Laravel Doctrine) — e.g. sin, cos, tan, asin, acos, atan, atan2, radians, degrees, pi.

  3. Confirm what your database actually exposes. MySQL 5.7+/8 and Postgres ship all of these natively; SQLite does not. This determines which functions you can safely rely on.

  4. First use case — a haversine distance query. This is what most people actually install a trig DQL bundle for:

    $qb->select('p.id', 'p.lat', 'p.lng')
        ->addSelect(
            '(6371 * ACOS(
                COS(RADIANS(:lat)) * COS(RADIANS(p.lat)) * COS(RADIANS(p.lng) - RADIANS(:lng))
              + SIN(RADIANS(:lat)) * SIN(RADIANS(p.lat))
            )) AS HIDDEN distance'
        )
        ->having('distance < :radius')
        ->setParameter('lat', $lat)
        ->setParameter('lng', $lng)
        ->setParameter('radius', 25);
    

    If that parses and returns rows, the bundle is wired up correctly.

Implementation Patterns

Register functions globally, use per-query. Add them all to config/doctrine.php under managers.default.custom_dql_functions so every EntityManager knows about them. Registering via $em->getConfiguration()->addCustomNumericFunction() at runtime works but breaks when Laravel Doctrine reboots the EM under Octane or in queue workers.

Repository method as the boundary. Wrap the trig DQL in a repository so callers don't accidentally reference HAVERSINE on a query missing the joined columns:

class LocationRepository
{
    public function nearby(float $lat, float $lng, float $radiusKm): array
    {
        return $this->createQueryBuilder('l')
            ->addSelect('(6371 * ACOS(...)) AS HIDDEN distance')
            ->having('distance <= :radius')
            ->setParameter('radius', $radiusKm)
            ->setMaxResults(50)
            ->getQuery()
            ->getResult();
    }
}

Bearing / heading calculations. Combine ATAN2 with DEGREES and RADIANS to compute a compass bearing. Postgres, MySQL, and MariaDB all accept ATAN2(y, x) with the same argument order — but verify your DQL function class passes the two children through in order.

Cyclic / periodic aggregation. If you're building reporting on periodic data (seasonal sales, scheduled events), use SIN/COS to bucket records onto unit circles for clustering. This avoids a PHP-side pass over every row.

Combine with HIDDEN. Always alias computed trig expressions with AS HIDDEN <name> so they can be used in HAVING or ORDER BY without polluting the selected columns. Without HIDDEN, Doctrine's hydration will try to map the scalar to an entity.

Pagination-safe pattern. Use ->setMaxResults() + ->setFirstResult() on the trig query, not ->getResult() followed by PHP slicing — the HAVING clause does the filtering in SQL, and you want the DB to do the work.

Geo bounding-box pre-filter. Cheap win: pre-filter by lat/lng box in WHERE (indexed columns), then apply the haversine HAVING filter. Reduces the trig work dramatically on large tables:

->where('p.lat BETWEEN :minLat AND :maxLat')
->andWhere('p.lng BETWEEN :minLng AND :maxLng')

Gotchas and Tips

SQLite has no trig functions. Laravel's in-memory test suite uses SQLite by default. Your haversine query will throw no such function: ACOS in tests. Either run tests against MySQL/Postgres, or register a SQLite UDF via PDO::sqliteCreateFunction() in a test base class. Don't assume this bundle ports your query to SQLite.

RADIANS vs DEGREES — the classic bug. Haversine requires radians on every input; if any call site passes degrees to SIN/COS, you get subtly wrong distances that don't error out. Centralize unit conversion in the repository method so there's exactly one place to fix.

No index usage. Any expression using trig functions is opaque to the query planner. Even a composite index on (lat, lng) won't help COS(RADIANS(p.lat)). Use the bounding-box pre-filter described above; the trig function then only runs on the reduced candidate set.

Order of ATAN2 arguments. The bundle likely maps ATAN2(a, b) to ATAN2(a, b) in SQL (y, x). Different math libraries disagree on the order. Unit-test one known heading before trusting it in production.

MySQL ATAN2 vs ATAN. Older MySQL versions don't have ATAN2 — only ATAN(a, b) for two-argument. If you're on MariaDB or MySQL < 5.6, register the function against ATAN instead. Check SELECT VERSION() before deploying.

Register function aliases with the exact case the bundle uses. DQL function names are case-sensitive at the parser level. If the bundle registers SinFunction under sin, then SIN(...) in DQL may not resolve. Use the alias exactly as the bundle's DI extension declares it.

Precision at the poles / antipodes. ACOS of a value slightly outside [-1, 1] (from float rounding in the haversine formula) throws in MySQL strict mode. Wrap in LEAST(1, GREATEST(-1, expr)) if you're seeing sporadic math errors near antipodal points.

Debugging parse errors. When a DQL function isn't found, Doctrine throws Parser: Expected known function, got 'SIN'. That means registration failed, not that the SQL is broken — check config/doctrine.php first, then php artisan config:clear, then confirm the FunctionNode class actually extends Doctrine\ORM\Query\AST\Functions\FunctionNode and not a Symfony-specific wrapper.

Extension point. If the bundle doesn't ship a function you need (e.g. HAVERSINE prebuilt, or ACOT), extend FunctionNode yourself and drop it into the same custom_dql_functions array. Copy the pattern from the bundle's SinFunction — it's ~30 lines of getSql() + parse().

Octane / queue worker caveat. DQL functions registered at runtime via addCustomNumericFunction() are lost between requests. Registering them in config/doctrine.php survives Octane restarts because Laravel Doctrine rebuilds the EM from config. If you're using runtime registration for conditional functions, register them in a service provider's boot() guarded by config('doctrine.managers.default.custom_dql_functions').

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.
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator