PHPackages                             ray/media-query - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. ray/media-query

ActiveLibrary[Database &amp; ORM](/categories/database)

ray/media-query
===============

PHP interface-based SQL framework

1.0.3(3mo ago)11220.7k—7.9%82MITPHPPHP ^8.2CI failing

Since Mar 12Pushed 3mo ago3 watchersCompare

[ Source](https://github.com/ray-di/Ray.MediaQuery)[ Packagist](https://packagist.org/packages/ray/media-query)[ RSS](/packages/ray-media-query/feed)WikiDiscussions 1.x Synced 1mo ago

READMEChangelog (10)Dependencies (12)Versions (45)Used By (2)

Ray.MediaQuery
==============

[](#raymediaquery)

[![codecov](https://camo.githubusercontent.com/741c2069ee7bdebdf25e50e24a2d798400a62854f463eea26037c9a3254f0c11/68747470733a2f2f636f6465636f762e696f2f67682f7261792d64692f5261792e4d6564696151756572792f6272616e63682f312e782f67726170682f62616467652e7376673f746f6b656e3d51424f504355504a5156)](https://codecov.io/gh/ray-di/Ray.MediaQuery)[![Type Coverage](https://camo.githubusercontent.com/5ccbe4de30e70690c7a9635c85683ddd60b2e9bbf5451547fe35d0c716ecabcf/68747470733a2f2f73686570686572642e6465762f6769746875622f7261792d64692f5261792e4d6564696151756572792f636f7665726167652e737667)](https://shepherd.dev/github/ray-di/Ray.MediaQuery)[![Continuous Integration](https://github.com/ray-di/Ray.MediaQuery/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/ray-di/Ray.MediaQuery/actions/workflows/continuous-integration.yml)

Interface-Driven SQL for PHP
----------------------------

[](#interface-driven-sql-for-php)

**Ray.MediaQuery lets SQL be SQL and Objects be Objects.**

Traditional ORMs try to hide SQL behind object abstractions. Ray.MediaQuery takes a different approach:

```
// 1. Define your interface (and Entity)
interface UserQueryInterface
{
    #[DbQuery('user_item')]
    public function item(string $id): ?User;
}

class User
{
    public function __construct(
        public readonly string $id,
        public readonly string $name
    ) {}
}

// 2. Write your SQL
-- user_item.sql
SELECT id, name FROM users WHERE id = :id

// 3. Use it (no implementation needed!)
$userQuery = $injector->getInstance(UserQueryInterface::class);
$user = $userQuery->item('user-123');
```

Why Ray.MediaQuery?
-------------------

[](#why-raymediaquery)

### Zero Implementation Code

[](#zero-implementation-code)

Define interfaces, get working repositories. No boilerplate, no mapping configuration.

### SQL Excellence Without Compromise

[](#sql-excellence-without-compromise)

Use the full power of your database - window functions, CTEs, custom functions. If it runs in your database, it works with Ray.MediaQuery.

### Rich Domain Objects via Dependency Injection

[](#rich-domain-objects-via-dependency-injection)

**Traditional ORMs give you data objects. Business logic ends up in controllers.**Ray.MediaQuery transforms SQL results into rich domain objects through factories with dependency injection.

```
interface OrderRepository
{
    #[DbQuery('order_detail', factory: OrderDomainFactory::class)]
    public function getOrder(string $id): Order;
}

// Factory injects services and enriches data from SQL
class OrderDomainFactory
{
    public function __construct(
        private TaxService $taxService,
        private InventoryService $inventory,
        private RuleEngine $rules,
    ) {}

    public function factory(string $id, float $subtotal): Order
    {
        return new Order(
            id: $id,
            subtotal: $subtotal,
            tax: $this->taxService->calculate($subtotal),
            canShip: $this->inventory->check($id),
            rules: $this->rules,
        );
    }
}

// Domain object with business logic
class Order
{
    public function __construct(
        public string $id,
        public float $subtotal,
        public float $tax,
        public bool $canShip,
        private RuleEngine $rules,
    ) {}

    public function getPriority(): string
    {
        return $this->rules->calculatePriority($this);
    }
}
```

> See [BDR Pattern Guide](./BDR_PATTERN.md) for the architectural approach behind this design.

### Test Each Layer Independently

[](#test-each-layer-independently)

SQL queries, factories, and domain objects can all be tested in isolation. When each layer works, the combination works.

### AI-Era Transparency

[](#ai-era-transparency)

Unlike ORM magic, everything is explicit and readable - perfect for AI assistants to understand and help with your codebase.

Core Concept: Interface-Driven Design
-------------------------------------

[](#core-concept-interface-driven-design)

Ray.MediaQuery binds PHP interfaces directly to SQL execution. No abstract query builders, no hidden SQL generation, no runtime surprises.

```
interface TodoRepository
{
    #[DbQuery('add_todo')]
    public function add(string $id, string $title): void;

    #[DbQuery('todo_list')]
    /** @return array */
    public function findByUser(string $userId): array;

    #[DbQuery('stats', factory: StatsFactory::class)]
    public function getStats(string $userId): UserStats;
}
```

The framework handles:

- SQL file discovery and execution
- Parameter binding with type conversion
- Result hydration to entities or arrays
- Factory-based transformations with DI
- Transaction management

You focus on:

- Defining clear interfaces
- Writing efficient SQL
- Implementing business logic

Quick Start
-----------

[](#quick-start)

### Installation

[](#installation)

```
composer require ray/media-query
```

### Basic Setup

[](#basic-setup)

```
use Ray\Di\AbstractModule;
use Ray\Di\Injector;
use Ray\MediaQuery\Annotation\DbQuery;
use Ray\MediaQuery\MediaQuerySqlModule;
use Ray\AuraSqlModule\AuraSqlModule;

// 1. Configure in your module
class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(
            new MediaQuerySqlModule(
                interfaceDir: '/path/to/query/interfaces',
                sqlDir: '/path/to/sql/files'
            )
        );

        $this->install(
            new AuraSqlModule(
                'mysql:host=localhost;dbname=app',
                'username',
                'password'
            )
        );
    }
}

// 2. Define repository interface
interface UserRepository
{
    #[DbQuery('user_add')]
    public function add(string $id, string $name): void;

    #[DbQuery('user_find')]
    public function find(string $id): ?User;
}

// 3. Write SQL files
-- user_add.sql
INSERT INTO users (id, name) VALUES (:id, :name)

-- user_find.sql
SELECT * FROM users WHERE id = :id

// 4. Get instance and use (no implementation needed!)
$injector = new Injector(new AppModule());
$userRepo = $injector->getInstance(UserRepository::class);

$userRepo->add('user-123', 'Alice');
$user = $userRepo->find('user-123');
```

Advanced Features
-----------------

[](#advanced-features)

### Result Mapping &amp; Entity Hydration

[](#result-mapping--entity-hydration)

Ray.MediaQuery automatically hydrates query results based on your return type declarations:

**Single Entity:**

```
interface UserRepository
{
    #[DbQuery('user_find')]
    public function find(string $id): ?User;  // Returns User or null
}

class User
{
    public function __construct(
        public readonly string $id,
        public readonly string $name,
        public readonly string $email
    ) {}
}
```

**Entity Array:**

```
interface UserRepository
{
    #[DbQuery('user_list')]
    /** @return array */
    public function findAll(): array;  // Returns User[]
}
```

**Raw Array (single row):**

```
interface UserRepository
{
    #[DbQuery('user_stats', type: 'row')]
    public function getStats(string $id): array;  // ['total' => 10, 'active' => 5]
}
```

**Raw Array (multiple rows):**

```
interface UserRepository
{
    #[DbQuery('user_list')]
    public function listRaw(): array;  // [['id' => '1', ...], ['id' => '2', ...]]
}
```

**Constructor Property Promotion (Recommended):**

Use constructor property promotion for type-safe, immutable entities:

```
final class Invoice
{
    public function __construct(
        public readonly string $id,
        public readonly string $title,
        public readonly string $userName,      // camelCase property
        public readonly string $emailAddress,  // camelCase property
    ) {}
}

// SQL: SELECT id, title, user_name, email_address FROM invoices
// Ray.MediaQuery handles snake_case → camelCase conversion automatically
```

For PHP 8.4+, use readonly classes:

```
final readonly class Invoice
{
    public function __construct(
        public string $id,
        public string $title,
        public string $userName,
        public string $emailAddress,
    ) {}
}
```

### Factory Pattern for Complex Objects

[](#factory-pattern-for-complex-objects)

Use factories when entities need computed properties or injected services:

**Basic Factory:**

```
interface OrderRepository
{
    #[DbQuery('order_detail', factory: OrderFactory::class)]
    public function getOrder(string $id): Order;
}

class OrderFactory
{
    public function factory(string $id, float $amount): Order
    {
        return new Order(
            id: $id,
            amount: $amount,
            tax: $amount * 0.1,      // Computed
            total: $amount * 1.1,    // Computed
        );
    }
}
```

**Factory with Dependency Injection:**

```
class OrderFactory
{
    public function __construct(
        private TaxCalculator $taxCalc,       // Injected
        private ShippingService $shipping,    // Injected
    ) {}

    public function factory(string $id, float $amount, string $region): Order
    {
        return new Order(
            id: $id,
            amount: $amount,
            tax: $this->taxCalc->calculate($amount, $region),
            shipping: $this->shipping->calculate($region),
        );
    }
}
```

**Polymorphic Entities:**

```
class UserFactory
{
    public function factory(string $id, string $type, string $email): UserInterface
    {
        return match ($type) {
            'free' => new FreeUser($id, $email, maxStorage: 100),
            'premium' => new PremiumUser($id, $email, maxStorage: 1000),
        };
    }
}
```

> **Architecture Pattern**: Factories enable the [**BDR Pattern**](./BDR_PATTERN.md) - combining efficient SQL with rich domain objects through dependency injection.

### Smart Parameter Handling

[](#smart-parameter-handling)

**DateTime Automatic Conversion:**

```
interface TaskRepository
{
    #[DbQuery('task_add')]
    public function add(string $title, DateTimeInterface $createdAt = null): void;
}

// SQL: INSERT INTO tasks (title, created_at) VALUES (:title, :createdAt)
// DateTime converted to: '2024-01-15 10:30:00'
// null injects current time automatically
```

**Value Objects:**

```
class UserId implements ToScalarInterface
{
    public function __construct(private int $value) {}

    public function toScalar(): int
    {
        return $this->value;
    }
}

interface MemoRepository
{
    #[DbQuery('memo_add')]
    public function add(string $memo, UserId $userId): void;
}

// UserId automatically converted via toScalar()
```

**Parameter Injection:**

```
interface TodoRepository
{
    #[DbQuery('todo_add')]
    public function add(string $title, Uuid $id = null): void;
}

// null triggers DI: Uuid is generated and injected automatically
```

### Input Object Flattening

[](#input-object-flattening)

Structure your input while keeping SQL simple with `Ray.InputQuery`.

> **Note**: This feature requires the `ray/input-query` package, which is already included as a dependency.

```
use Ray\InputQuery\Attribute\Input;

class UserInput
{
    public function __construct(
        #[Input] public readonly string $givenName,
        #[Input] public readonly string $familyName,
        #[Input] public readonly string $email
    ) {}
}

class TodoInput
{
    public function __construct(
        #[Input] public readonly string $title,
        #[Input] public readonly UserInput $assignee,  // Nested
        #[Input] public readonly ?DateTimeInterface $dueDate
    ) {}
}

interface TodoRepository
{
    #[DbQuery('todo_create')]
    public function create(TodoInput $input): void;
}

// Input flattened automatically:
// :title, :givenName, :familyName, :email, :dueDate
```

### Pagination

[](#pagination)

Enable lazy-loaded pagination with the `#[Pager]` attribute:

**Basic Pagination:**

```
use Ray\MediaQuery\Annotation\DbQuery;
use Ray\MediaQuery\Annotation\Pager;
use Ray\MediaQuery\Pages;

interface ProductRepository
{
    #[DbQuery('product_list'), Pager(perPage: 20, template: '/{?page}')]
    public function getProducts(): Pages;
}

$pages = $productRepo->getProducts();
$count = count($pages);  // Executes COUNT query
$page = $pages[1];       // Executes SELECT with LIMIT/OFFSET

// Page object properties:
// $page->data          // Items for this page
// $page->current       // Current page number
// $page->total         // Total pages
// $page->hasNext       // Has next page?
// $page->hasPrevious   // Has previous page?
// (string) $page       // Pager HTML
```

**Dynamic Page Size:**

```
interface ProductRepository
{
    #[DbQuery('product_list'), Pager(perPage: 'perPage', template: '/{?page}')]
    public function getProducts(int $perPage): Pages;
}
```

**With Entity Hydration:**

```
interface ProductRepository
{
    #[DbQuery('product_list'), Pager(perPage: 20)]
    /** @return Pages */
    public function getProducts(): Pages;
}

// Each page's data is hydrated to Product entities
```

### Direct SQL Execution

[](#direct-sql-execution)

For advanced use cases, inject `SqlQueryInterface` directly:

```
use Ray\MediaQuery\SqlQueryInterface;

class CustomRepository
{
    public function __construct(
        private SqlQueryInterface $sqlQuery
    ) {}

    public function complexQuery(array $params): array
    {
        return $this->sqlQuery->getRowList('complex_query', $params);
    }
}
```

**Available Methods:**

- `getRow($queryId, $params)` - Single row
- `getRowList($queryId, $params)` - Multiple rows
- `exec($queryId, $params)` - Execute without result
- `getStatement()` - Get PDO statement
- `getPages()` - Get paginated results

Philosophy: Boundaries That Dissolve
------------------------------------

[](#philosophy-boundaries-that-dissolve)

Ray.MediaQuery doesn't fight the impedance mismatch - it dissolves it. SQL and Objects don't need to pretend the other doesn't exist. They can work together, each doing what they do best.

This is more than a technical solution. It's a recognition that different paradigms can coexist harmoniously when we stop trying to force one to be the other.

Real-World Benefits
-------------------

[](#real-world-benefits)

- **Performance**: Write optimized SQL without ORM overhead
- **Maintainability**: Clear separation of concerns
- **Testability**: Test SQL and PHP logic independently
- **Flexibility**: Refactor interfaces without touching SQL
- **Transparency**: Every query is visible and optimizable

Learn More
----------

[](#learn-more)

- [BDR Pattern Guide](./BDR_PATTERN.md)
- [Demo Application](./demo/)

###  Health Score

59

—

FairBetter than 99% of packages

Maintenance78

Regular maintenance activity

Popularity43

Moderate usage in the ecosystem

Community23

Small or concentrated contributor base

Maturity78

Established project with proven stability

 Bus Factor1

Top contributor holds 92.1% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~46 days

Recently: every ~16 days

Total

39

Last Release

114d ago

Major Versions

0.17.1 → v1.0.0-rc12025-08-01

PHP version history (6 changes)0.1.0PHP ^7.4 || ^8.0

0.3.0PHP ^7.3 || ^8.0

0.9.0PHP ^8.0

0.12.1PHP ^8.1

0.14.0PHP &gt;=8.1 &lt;8.4

v1.0.0-rc3PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/db4fc75ffc631168d0d7143b6f2c24b1534dfb921212bd851c026c5cbbb1344d?d=identicon)[koriym](/maintainers/koriym)

---

Top Contributors

[![koriym](https://avatars.githubusercontent.com/u/529021?v=4)](https://github.com/koriym "koriym (469 commits)")[![jingu](https://avatars.githubusercontent.com/u/892913?v=4)](https://github.com/jingu "jingu (19 commits)")[![NaokiTsuchiya](https://avatars.githubusercontent.com/u/17171732?v=4)](https://github.com/NaokiTsuchiya "NaokiTsuchiya (8 commits)")[![apple-x-co](https://avatars.githubusercontent.com/u/8497012?v=4)](https://github.com/apple-x-co "apple-x-co (4 commits)")[![mstysk](https://avatars.githubusercontent.com/u/2937579?v=4)](https://github.com/mstysk "mstysk (4 commits)")[![HajimeMat-AVAP](https://avatars.githubusercontent.com/u/55294484?v=4)](https://github.com/HajimeMat-AVAP "HajimeMat-AVAP (3 commits)")[![shotanue](https://avatars.githubusercontent.com/u/22065594?v=4)](https://github.com/shotanue "shotanue (1 commits)")[![yuki777](https://avatars.githubusercontent.com/u/177159?v=4)](https://github.com/yuki777 "yuki777 (1 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/ray-media-query/health.svg)

```
[![Health](https://phpackages.com/badges/ray-media-query/health.svg)](https://phpackages.com/packages/ray-media-query)
```

###  Alternatives

[nelmio/api-doc-bundle

Generates documentation for your REST API from attributes

2.3k63.6M233](/packages/nelmio-api-doc-bundle)[friendsofsymfony/elastica-bundle

Elasticsearch PHP integration for your Symfony project using Elastica

1.3k17.2M47](/packages/friendsofsymfony-elastica-bundle)[humbug/box

Fast, zero config application bundler with PHARs.

1.3k801.5k69](/packages/humbug-box)[kimai/kimai

Kimai - Time Tracking

4.6k7.4k1](/packages/kimai-kimai)[thecodingmachine/graphqlite

Write your GraphQL queries in simple to write controllers (using webonyx/graphql-php).

5723.1M30](/packages/thecodingmachine-graphqlite)[bolt/core

🧿 Bolt Core

585142.5k54](/packages/bolt-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
