Skip to content

Eloquent: Getting Started

Introduction

Doppar features its own powerful data interaction tool, Eloquent, an object-relational mapper (ORM), which simplifies and enhances the way you work with your database. With ORM, every database table is linked to a dedicated "Data Model" that serves as your gateway to managing table data seamlessly. Beyond just fetching records, Doppar Data Mapper empowers you to effortlessly insert, update, and delete records, making database interactions intuitive and efficient. Whether you're building complex queries or handling simple data operations, ORM ensures a smooth and enjoyable experience, tailored to streamline your development workflow.

Creating Model Classes

To get started, let's create an Eloquent model. Models typically live in the app\Models directory and extend the Phaseolies\Database\Eloquent\Model class. You may use the make:model Pool command to generate a new model:

bash
php pool make:model Post

This command will generate a new model inside App\Models directory. Models generated by the make:model command will be placed in the app/Models directory. Let's see a basic model class.

php
<?php

namespace App\Models;

use Phaseolies\Database\Eloquent\Model;

class Post extends Model
{
    //
}

If you pass the -m option, it will create a migration file for your associative model, like:

bash
php pool make:model Post -m

Model Properties

Before diving into Doppar's data management capabilities, it’s important to familiarize yourself with some key model properties that shape how your data is handled. Doppar offers the flexibility to customize these properties to suit your specific needs. Key properties include $pageSize, which controls the number of records displayed per page; $primaryKey, which defines the unique identifier for your table; $table, which specifies the database table associated with the model; $creatable, which determines whether new records can be added; and $unexposable and $timeStamps, which allows you to hide sensitive or irrelevant data from being exposed and handle datetime columns. With Doppar, you have full control to tweak these properties, ensuring your data interactions are both efficient and secure. Let's see the User model as for example.

$primaryKey

Specifies the column name that serves as the unique identifier for the table. By default, this is set to 'id', but it can be customized if your table uses a different primary key.

php
<?php

namespace App\Models;

use Phaseolies\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * The primary key associated with the table.
     *
     * @var int
     */
    protected $primaryKey = 'id';
}

$table

Specifies the database table associated with this model

php
<?php

namespace App\Models;

use Phaseolies\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'posts';
}

$creatable

Specifies which attributes can be mass-assigned when creating or updating records.This helps prevent mass assignment vulnerabilities by explicitly defining safe fields. Only the attributes listed here can be set in bulk operations.

php
<?php

namespace App\Models;

use Phaseolies\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Creatable Attributes
     *
     * @var array
     */
    protected $creatable = ['title', 'status', 'description'];
}

$unexposable

Specifies which attributes should be hidden when the model is converted to an array or JSON or Collection. This is particularly useful for hiding sensitive information, such as passwords, from being exposed in API responses or other outputs.

php
<?php

namespace App\Models;

use Phaseolies\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Unexposable Attributes
     *
     * @var array
     */
    protected $unexposable = ['password'];
}

$timeStamps

Indicates whether the model should maintain timest(created_at and updated_at fields.).

php
<?php

namespace App\Models;

use Phaseolies\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Indicates if the model should be timestamped.
     *
     * @var bool
     */
    protected $timeStamps = true;
}

#[CastToDate]

By default, $timeStamps uses the datetime format. However, you can convert it to a date format by using Phaseolies\Utilities\Casts\CastToDate like this:

php
<?php

namespace App\Models;

use Phaseolies\Utilities\Casts\CastToDate;
use Phaseolies\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Indicates if the model should be timestamped.
     *
     * @var bool
     */
    #[CastToDate]
    protected $timeStamps = true;
}

Query Using ORM

Retrieving Models

Once you have created a model and its associated database table, you are ready to start retrieving data from your database. You can think of each Eloquent model as a powerful query builder allowing you to fluently query the database table associated with the model. The model's all method will retrieve all of the records from the model's associated database table:

php
<?php

use App\Models\Post;

$posts = Post::all();

// Or you can use direct get() method
$posts = Post::get();

foreach (Post::all() as $post) {
    echo $post->title;
}

Expending Queries

The all method in Eloquent fetches every record from a model’s corresponding table. However, because Eloquent models double as query builders, you can refine queries using conditions and retrieve only the matching results by calling the get method.

php
$posts = Post::query()
    ->where('status', '=', true)
    ->orderBy('title')
    ->limit(10)
    ->get();

You can write query with multiple where condition by chaining multiple where with the queries

php
Post::query()
    ->where('title', '=', 'Sincere Littel')
    ->where('status', '=', 1)
    ->first();

Collections

As observed, Eloquent methods such as all and get are designed to fetch multiple records from the database. Rather than returning a standard PHP array, these methods yield an instance of Phaseolies\Support\Collection, providing a rich set of tools to work with the retrieved models efficiently.

If you want to convert collection to array, you can use toArray() method.

php
$posts = Post::query()
    ->where('status', '=', true)
    ->orderBy('title')
    ->limit(10)
    ->get()
    ->toArray();

Fetch the First Records

To fetch the first records you can use first() function as like below

php
Post::query()->first();

Actually this above query returns the Post model object so you can directly fetch its attributes like

php
Post::query()->first()?->title ?? 'defult';

GroupBy and OrderBy

You can also use groupBy() and orderBy() to handle your records like

php
User::query()->orderBy('id', 'desc')->groupBy('name')->get();

random()

The random() method in Doppar's Eloquent ORM extension allows you to retrieve a random subset of records from the database. It's particularly useful when you want to display randomized content, such as featured products, suggested users, or shuffled items in a feed.

Internally, random() applies an ORDER BY RAND() clause to the query and limits the result set using the provided number.

Here's an example:

php
User::query()
    ->random(10)
    ->get();

This will return 10 random users from the users table.

toSql()

The toSql() method in Eloquent is a useful tool when you want to inspect the raw SQL query that would be executed for a given Eloquent query. Instead of retrieving the results from the database, toSql() returns the SQL statement as a string, allowing you to debug or log the query before it's run. This is especially helpful during development when you need to understand how your query builder chain is being translated into SQL.

Here's an example:

php
User::query()
    ->where('status', '=', 'active')
    ->toSql();

This will output:

sql
select * from `users` where `status` = ?

Using toSql() helps you optimize and troubleshoot queries by giving insight into what Doppar is sending to the database under the hood.

find()

The find() method in Eloquent is used to retrieve a single record from the database by its primary key. It offers a quick and efficient way to look up a specific model instance without having to write a full where clause. If a matching record is found, an instance of the model is returned; otherwise, null is returned.

Here’s a basic example:

php
$user = User::find(1);

In this case, Eloquent returns a collection of User instances corresponding to the given IDs. find() is ideal when you know the exact primary key(s) you're looking for and want a concise way to retrieve the corresponding record(s).

This is the sort version of this query

php
$user = User::query()->where('id', '=', 1)->first();

You can also pass multiple primary key to fetch data like this way

php
$users = User::find([1, 2, 3]);

count()

To see how many records your table has, you can use count() function as like

php
User::count();
User::query()->orderBy('id', 'desc')->groupBy('name')->count();
User::query()->where('active', '=', true)->count();

Newest and Oldest Records

Doppar makes it convenient to retrieve the most recent or earliest records from a database using the newest() and oldest() query methods. These methods sort the results based on a specified column, with id being the default field if none is provided.

To fetch the latest records (in descending order):

php
User::query()->newest()->get();

This retrieves the most recently added users based on the id column by default. If you want to sort by a different column, you can pass it as an argument:

php
User::query()->newest('name')->get();

Similarly, to retrieve the oldest records (in ascending order):

php
User::query()->oldest()->get();

Again, you can specify a custom column to determine what "oldest" means in your context:

php
User::query()->oldest('id')->get();

These methods offer a clean and readable way to sort query results by time or any relevant field, enhancing code clarity while maintaining flexibility.

Selecting Specific Columns

In many cases, you may not need to retrieve every column from a table—especially when working with large datasets. Eloquent's select() method allows you to specify exactly which columns you want to fetch, helping optimize performance and reduce memory usage.

Here are a few examples:

php
// Selecting specific columns using an array
User::query()->select(['name', 'email'])->get();

// Selecting specific columns using multiple arguments
User::query()->select('name', 'email')->get();

Both of these will return only the name and email fields for each user, excluding all other columns.

You can also use select() in more advanced queries, such as when grouping results:

php
Post::query()
    ->select('title', 'COUNT(*) as count')
    ->groupBy('category_id')
    ->get();

Using select() helps tailor your queries to fetch only the data you actually need, improving both efficiency and clarity in your code.

selectRaw()

The selectRaw() method in Doppar's query builder provides a powerful way to include raw SQL expressions in your queries. It's especially useful when you need to perform calculations, use SQL functions, or include complex expressions that go beyond Eloquent's default capabilities.

Basic Usage

You can use selectRaw() to write a custom SQL expression directly in the SELECT clause. For example, to count the total number of orders:

php
use App\Models\Order;

$total = Order::query()
    ->selectRaw('COUNT(*) as order_count')
    ->first();

echo $total->order_count;

This will execute a query similar to:

sql
SELECT COUNT(*) as order_count FROM orders;

Calculated Columns

You can also perform calculations using existing fields in the database. For instance, to calculate the total value of each order:

php
$orders = Order::query()
    ->selectRaw('price * quantity as total_value')
    ->get();

foreach ($orders as $order) {
    echo $order->total_value;
}

This calculates a total_value field on the fly for each record using the price and quantity columns.

Chaining Multiple selectRaw() Calls

You can chain multiple selectRaw() calls to build a SELECT clause that includes several raw expressions. Each call adds to the final SQL:

php
$orders = Order::query()
    ->selectRaw('SUM(price) as total_sales')
    ->selectRaw('AVG(quantity) as average_quantity')
    ->selectRaw('MAX(price) as highest_price')
    ->first();

This returns an object with aggregated values such as total sales, average quantity, and the highest price.

⚠️ Security Tip: When using raw expressions, especially with user input, always bind parameters or sanitize values to protect against SQL injection.

Using Bindings with selectRaw()

Scenario: You're running an e-commerce platform. You store orders in an orders table with these columns:

  • price: base price of an item
  • category_id: the category the product belongs to
  • status: whether the order is completed (true) or not
  • created_at: timestamp of the order

You want to:

  • Get the maximum order value after tax (e.g. 8%) and after VAT (e.g. 20%)
  • Only consider completed orders
  • Group results by category

Now assume "For each product category, what's the highest order value including tax and VAT, from completed orders?"

php
$orders = Order::query()
    ->selectRaw('MAX(price * ?) AS total_with_tax', [1.08]) // Apply 8% tax
    ->selectRaw('MAX(price * ?) AS total_with_vat', [1.20]) // Apply 20% VAT
    ->where('status', '=', true)
    ->groupByRaw('category_id')
    ->get();

Example output

category_idtotal_with_taxtotal_with_vat
1108.00120.00
2216.00240.00
3162.00180.00

Why Use selectRaw Here?

  • You need to run math operations (price * taxRate) directly in SQL.
  • You want to bind the tax rate dynamically (?) to avoid hardcoding values.
  • You’re leveraging SQL’s aggregate function (MAX()) for reporting.

Bindings are passed as an array to selectRaw() and must match the number of ? placeholders in the SQL expression.

whereRaw()

The whereRaw() method allows you to write a raw SQL WHERE clause directly into an Eloquent query. This is useful when your query requires SQL features that aren't easily expressed using Eloquent's fluent methods — such as complex conditions, custom SQL functions, or case-insensitive matching.

You pass a raw SQL string as the first argument, and an optional array of bindings as the second. This helps maintain parameter binding and protects against SQL injection.

Here’s an example:

php
$users = User::query()
    ->whereRaw('LOWER(name) LIKE LOWER(?)', ['%jewel%'])
    ->get();

This query returns all users whose name matches “jewel” in a case-insensitive way, by applying the LOWER() SQL function to both the column and the search term.

orderByRaw()

The orderByRaw() method allows you to write a raw SQL ORDER BY clause directly into an Eloquent query. This is useful when you need advanced sorting logic that can't be easily represented using Eloquent’s fluent orderBy() method — such as ordering by multiple columns with different sort directions or using SQL functions.

You pass a raw SQL string as the first argument, which defines the custom sorting logic. Because the input is raw SQL, it gives you full control over the order clause — but it’s important to ensure the syntax is correct and secure.

Here’s an example:

php
Employee::query()
    ->orderByRaw('salary DESC, name ASC')
    ->get();

Use orderByRaw() when your ordering requirements exceed the capabilities of standard Eloquent methods.

groupByRaw()

The groupByRaw() method allows you to define a raw SQL GROUP BY clause in an Eloquent query. It’s especially useful when you need to group results by SQL functions or multiple columns that can't be easily handled using Doppar’s fluent groupBy() method.

You pass a raw SQL string as the argument, which gives you full control over the grouping logic.

Here’s an example:

php
User::query()
    ->selectRaw('COUNT(*) as total, YEAR(created_at) as year, MONTH(created_at) as month')
    ->groupByRaw('YEAR(created_at), MONTH(created_at)')
    ->orderByRaw('year DESC, month DESC')
    ->get();

This query:

  • Counts how many users were created in each month,
  • Groups the results by year and month using the YEAR() and MONTH() SQL functions,
  • Orders the grouped results from most recent to oldest.

This is useful for generating reports, monthly user activity, or analytics dashboards.

omit()

Excludes specific columns from the SELECT query. This method is useful when you want to retrieve all columns except certain ones, such as timestamp or metadata fields.

php
<?php

use App\Models\Post;

$posts = Post::query()
    ->omit('created_at', 'updated_at')
    ->get();

In the example above, all columns will be selected except created_at and updated_at. You can also pass an array instead of variadic arguments:

php
Post::query()->omit(['deleted_at', 'archived_at'])->get();

If the query initially selects all columns using *, omit() will first resolve the actual column names via the model's table schema and then exclude the specified ones.

exists()

To check whether a specific row exists in your database, you can use the exists() function. This method returns a boolean value (true or false) based on whether the specified condition matches any records. Here's an example:

php
// Returns `true` if a matching row exists, otherwise `false`.
User::query()->where('id', '=', 1)->exists();

whereIn()

The whereIn() method filters records where a column's value matches any value in the given array.

php
User::query()->whereIn('id', [1, 2, 4])->get();

This retrieves all users with id values of 1, 2, or 4.

orWhereIn()

The orWhereIn() method filters records where a column's value optionally matches any value in the given array.

php
User::query()->orWhereIn('id', [1, 2, 4])->get();

whereBetween()

The whereBetween() method in Doppar lets you filter records where a column’s value falls within a specified range. It’s particularly useful for working with numeric values or date/time columns, such as filtering records created within a certain time frame.

Example: Filter Users by Date Range

php
User::query()
    ->whereBetween('created_at', ['2025-02-29', '2025-04-29'])
    ->get();

This query will retrieve all users whose created_at timestamp falls between February 29, 2025 and April 29, 2025, inclusive.

You can use whereBetween() with any comparable column — such as prices, IDs, scores, or dates — to make your queries more expressive and efficient.

php
User::query()->whereBetween('id', [1, 10])->get();

This query retrieves all user records where the id is between 1 and 10, inclusive. That means it will return users with id values of 1, 2, 3, ..., 10.

whereNotBetween()

The whereNotBetween() method allows you to filter records where a given column's value do not falls within a specified range. This is commonly used for date or numerical ranges.

php
User::query()
    ->whereNotBetween('created_at', ['2025-02-29', '2025-04-29'])
    ->get();

orWhereBetween()

Doppar allows you to construct expressive, flexible queries using methods like orWhereBetween(). This method is particularly useful when you want to retrieve records that match either a specific condition or fall within a certain range.

Example: Combine exact match with a range

php
Post::query()
    ->where('status', '=', 'published')
    ->orWhereBetween('views', [100, 500])
    ->get();

This will return all posts that are either published or have between 100 and 500 views, inclusive.

orWhereNotBetween()

Similarly, orWhereNotBetween() is used when you want to retrieve records that match a condition or fall outside a given range. This adds more control to your filtering logic.

php
Post::query()
    ->where('status', ,'=', 'published')
    ->orWhereNotBetween('views', [100, 500])
    ->get();

This query retrieves posts that are either published or have a view count less than 100 or greater than 500.

whereNull()

In Doppar, the whereNull() method is used to filter records where a specific column has no value—i.e., it contains NULL. This is useful when you're identifying incomplete or pending data.

Example: Find Posts Without a Created Date

php
Post::query()
    ->whereNull('created_at')
    ->get();

This query retrieves all posts where the created_at field is NULL, which might indicate drafts or records that haven't been finalized yet.

whereNotNull()

Conversely, whereNotNull() filters records where a given column does contain a value. It's ideal for fetching entries that are already completed or published.

Example: Fetch Published Posts

php
Post::query()
    ->whereNotNull('published_at')
    ->get();

This retrieves all posts that have a value in the published_at field, meaning they’ve been published.

orWhereNull()

The orWhereNull() method in Eloquent is used to add an OR condition to the query, checking if a column is NULL. In your example

php
Post::query()
    ->where('status', '=', 'draft')
    ->orWhereNull('reviewed_at')
    ->get();

orWhereNotNull()

The orWhereNotNull() method in Eloquent is used to add an OR condition to the query, checking if a column is not NULL. In your example:

php
Post::query()
    ->where('status', '=', 'draft')
    ->orWhereNotNull('reviewed_at')
    ->get();

pluck()

The pluck() method in Doppar is a convenient way to extract the values of a single column from your query results. Instead of returning full model instances, pluck() pulls out just the values from the specified field—perfect for generating simple lists.

Example: Get All Post Titles

php
Post::query()->pluck('title');

This will return a collection of all titles from the posts table, such as:

php
[
    "How to Use Doppar",
    "Getting Started with PHP",
    "Understanding Eloquent Queries"
]

DB::sql()

The DB::sql() method is used to insert raw SQL expressions into an Eloquent query. It’s useful when you need to perform calculations, use SQL functions, or alias complex expressions directly within a select(), orderBy(), or other query builder methods.

This is especially handy when Eloquent’s query builder doesn’t cover a specific SQL feature or function.

Here’s an example:

php
use Phaseolies\Support\Facades\DB;

Employee::query()
    ->select(
        'name',
        'salary',
        DB::sql("ROUND(salary * 7.5 / 100, 2) as tax_amount")
    )
    ->get();

In this case, DB::sql() adds a computed column named tax_amount that represents 7.5% of each employee’s salary, rounded to two decimal places.

You can use DB::sql() to extract, filter, or unquote JSON fields stored in your database. Here’s an example that extracts a nested value from a JSON column:

php
use Phaseolies\Support\Facades\DB;

User::query()
    ->select(
        'name',
        DB::sql("JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.preferences.notifications.email')) as email_notifications_enabled")
    )
    ->get();

In this case, the attributes column is a JSON object, and the query extracts the value at the path preferences.notifications.email, returning it as email_notifications_enabled.

toDictionary()

The toDictionary() method executes the query and returns the results as a simple key-value dictionary. This is useful when you need a flat, associative representation of a dataset — for example, populating dropdowns, lookups, or mapping IDs to labels.

You specify which column to use as the keys and which column to use as the values.

Here’s an example:

php
$users = User::query()
    ->toDictionary('id', 'name');

This returns a collection like:

php
[
    '1' => 'Alice',
    '2' => 'Bob',
    '3' => 'Charlie',
]

Internally, it selects only the specified key and value columns, iterates through the results, and builds a dictionary-style collection. This can be more memory-efficient than returning full model instances when you only need specific columns.

toDiff()

The toDiff() method calculates the difference between two numeric columns in your query and returns the result with a custom alias. This is especially useful when you want to compute values like profit, margin, balance, or other derived metrics directly within the database query.

You provide two column names, and an optional alias for the resulting column.

Here’s an example:

php
$profitMargins = Product::query()
    ->toDiff('price', 'cost', 'profit');

This query returns a collection of results where each item includes a single column: profit, which is the result of price - cost. Internally, the method generates a raw SQL expression like price - cost as profit and passes it to the query’s select() clause.

If no alias is provided, the result defaults to difference:

php
Product::query()
    ->toDiff('revenue', 'expenses');

Returns:

php
[
    ['difference' => 1200],
    ['difference' => 875],
    // ...
]

toTree()

The toTree() method transforms a flat list of records into a nested tree structure based on parent-child relationships. This is especially useful for hierarchical data like categories, menus, comments, or organizational units.

You provide the name of the primary key, the parent key column, and an optional children key to nest the relationships.

Here’s an example:

php
$categoryTree = Category::query()
    ->toTree('id', 'parent_id', 'children');

This will return a collection of top-level categories, each containing a children relation recursively filled with their respective subcategories.

Each item is recursively nested using the specified keys. The default key used to store children is children, but you can customize this as needed.

Structure:

If your flat data looks like this:

php
[
    ['id' => 1, 'name' => 'Electronics', 'parent_id' => null],
    ['id' => 2, 'name' => 'Laptops', 'parent_id' => 1],
    ['id' => 3, 'name' => 'Phones', 'parent_id' => 1],
    ['id' => 4, 'name' => 'Gaming', 'parent_id' => 2],
]

toTree() will convert it into:

json
[
  {
    "id": 1,
    "name": "Electronics",
    "children": [
      {
        "id": 2,
        "name": "Laptops",
        "children": [
          {
            "id": 4,
            "name": "Gaming",
            "children": []
          }
        ]
      },
      {
        "id": 3,
        "name": "Phones",
        "children": []
      }
    ]
  }
]

It automatically guards against circular references, throwing an exception if one is detected.

toRatio()

The toRatio() method calculates the ratio between two numeric columns and returns the result with a custom alias. This is particularly useful when you're tracking progress, rates, or percentages — such as completion rate, conversion rate, or usage metrics.

You provide the numerator and denominator column names, along with an optional alias and decimal precision for rounding.

Here’s an example:

php
$completionRatios = Project::query()
    ->toRatio(
        'completed_tasks',
        'total_tasks',
        'completion_rate',
        2
    );

This generates a query that returns a completion_rate column by dividing completed_tasks by total_tasks, rounded to 2 decimal places. Behind the scenes:

The query uses the SQL expression:

sql
ROUND(completed_tasks / NULLIF(total_tasks, 0), 2) as completion_rate

The NULLIF() ensures you don’t divide by zero — if total_tasks is 0, the result will be NULL instead of throwing a database error.

Default Usage:

If no alias or precision is provided, it defaults to:

php
->toRatio('wins', 'matches');

Which results in:

php
[
  { "ratio": 0.75 },
  { "ratio": 1.00 },
  { "ratio": null }
]

Use this method when you want clean, readable ratios without handling raw SQL manually.

Querying JSON Columns

Doppar's query builder includes powerful methods to search and extract values from JSON columns, using native MySQL JSON functions under the hood. This is useful when storing structured or dynamic data (e.g. settings, metadata, or preferences) in a single column.

Let’s assume a user record has the following field attributes as JSON like this:

php
{
  "theme": "dark",
  "language": "en",
  "notifications": {
    "sms": false,
    "email": true
  }
}

jsonEqual()

The jsonEqual() method allows you to query a specific value at a JSON path using JSON_EXTRACT, combined with JSON_UNQUOTE for proper comparison.

Here's how you might use it:

php
User::query()
    ->jsonEqual('attributes', '$.theme', 'dark')
    ->jsonEqual('attributes', '$.notifications.email', true)
    ->get();

This translates to SQL like:

sql
SELECT * FROM users
WHERE JSON_UNQUOTE(JSON_EXTRACT(`attributes`, ?)) = ?
AND JSON_UNQUOTE(JSON_EXTRACT(`attributes`, ?)) = ?

This method is ideal for exact value matching inside a JSON structure, including support for booleans.

jsonHas()

The jsonHas() method uses MySQL’s JSON_CONTAINS() function to check if a JSON path contains a specific value, including nested or array-based structures.

Example:

php
User::query()
    ->jsonHas('attributes', '$.theme', 'dark')
    ->jsonHas('attributes', '$.notifications.email', true)
    ->get();

This generates:

sql
SELECT * FROM users
WHERE JSON_CONTAINS(`attributes`, CAST(? AS JSON), '$.theme')
AND JSON_CONTAINS(`attributes`, CAST(? AS JSON), '$.notifications.email')

This is more flexible than jsonEqual() for contains-style checks, and works well with arrays or partial JSON fragments.

When you need full control over the SQL expression — such as when working with JSON functions — whereRaw() lets you write raw SQL where clauses directly into your query.

Here’s how you can query a specific value inside a JSON column using JSON_EXTRACT:

php
User::query()
    ->whereRaw("JSON_EXTRACT(attributes, '$.theme') = ?", ['dark'])
    ->get();

For safer, chainable, and reusable alternatives, consider jsonEqual() or jsonHas() if your use case fits.

selectRaw or DB::sql() for JSON Extraction

If you want to extract a value from a JSON column in your select statement, use DB::sql() or selectRaw():

php
User::query()
    ->select(DB::sql("JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.theme')) as theme_preference"))
    ->get();

Or, with selectRaw()

php
User::query()
    ->selectRaw("JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.theme')) as theme_preference")
    ->get();

This will give you a new column theme_preference in your result set, showing each user's selected theme.

iLike()

The iLike() method performs a case-insensitive pattern match on a given column using SQL’s LIKE operator, making it easy to filter results regardless of case differences.

This is particularly useful when you want to search for substrings in text columns without worrying about letter casing.

Example usage:

php
User::query()
    ->iLike('name', '%john%')
    ->orderBy('name', 'desc')
    ->get();

This query returns all users whose name contains "john", "John", "JOHN", etc., ordered by name descending.

How it works:

The method generates a raw SQL condition like this:

sql
LOWER(name) LIKE LOWER('%john%')

By lowering both the column and the pattern, it ensures the match is case-insensitive.

transformBy()

The transformBy() method allows you to add a custom transformation or calculated expression to your query results. You pass a callback that receives a fresh query builder instance, and you return either a raw SQL expression string or a builder that builds the expression. The result is added as a new aliased column in your query.

Usage examples

Concatenate first_name and last_name as full_name

php
User::query()
    ->transformBy(function ($query) {
        return "CONCAT(first_name, ' ', last_name)";
    }, 'full_name')
    ->get();

This appends a full_name column combining first_name and last_name with a space.

Select uppercase name from users table

php
User::query()
    ->transformBy(function ($builder) {
        return "UPPER(name)";
    }, 'upper_name')
    ->get();

This adds an upper_name column that returns the uppercase version of the name field.

Calculate net compensation with salary and bonus

php
Employee::query()
    ->transformBy(function ($builder) {
        return "(salary * 0.8) + (bonus * 1.2)";
    }, 'net_compensation')
    ->get();

This appends a net_compensation column calculating a weighted sum of salary and bonus for each employee.

Compute platform-weighted average engagement

php
Post::query()
    ->where('category_id', 3)
    ->where('created_at', '>', now()->subYear())
    ->transformBy(function ($builder) {
        return "AVG(0.6 * facebook_share + 0.3 * linkedin_share + 0.1 * twitter_share)";
    }, 'platform_weighted_avg')
    ->first();

This query filters posts from category 3 created within the past year, and appends a platform_weighted_avg column representing the weighted average engagement score across multiple social platforms.

wherePattern()

The wherePattern() method allows you to filter query results using SQL pattern matching techniques such as LIKE, REGEXP, or SIMILAR TO. It wraps a raw where clause for flexible, expressive string pattern filters.

Usage Examples

Match email format using REGEXP

php
$users = User::query()
    ->wherePattern('email', '^[a-z]+@demo\.com$', 'REGEXP')
    ->get();

This filters users whose email matches the pattern name@demo.com, where name consists of lowercase letters only.

Perform a case-insensitive match using LIKE

php
User::query()
    ->wherePattern('name', '%Jewel%', 'LIKE')
    ->whereRaw('LOWER(name) LIKE LOWER(?)', ['%Jewel%'])
    ->get();

This finds users whose name contains “Jewel”, regardless of case, by combining wherePattern() and a manual LOWER() comparison for extra control.

firstLastInWindow()

The firstLastInWindow() method allows you to compute window functions such as FIRST_VALUE and LAST_VALUE over a partitioned and ordered dataset. This is especially useful for analytics-style queries where you want to attach aggregated or reference values from within a group to each row.

Usage Example

Example: Get each employee's department highest salary

php
Employee::query()
    ->select('name', 'salary', 'department')
    ->firstLastInWindow(
        'salary',              // Column to get value from
        'salary DESC',         // Order salaries descending
        'department',          // Partition by department
        true,                  // true i.e., max salary in DESC order
        'department_max_salary' // Alias
    )
    ->orderBy('salary', 'desc')
    ->get();

This will return all employees, along with the maximum salary within their department as a new column called department_max_salary. If first was set to false, it would return the minimum (or last) based on the ordering.

This is a powerful tool for windowed analytics and reporting, giving you rich insights from grouped data without breaking the row-level context.

movingDifference()

Calculates the difference between a current value and the previous row's value in an ordered series using SQL LAG() function. This is useful for identifying changes over time or detecting trends.

Track daily energy consumption difference

php
EnergyLog::query()
    ->select('date', 'kilowatt_hours')
    ->movingDifference('kilowatt_hours', 'date', 'daily_usage_change')
    ->orderBy('date')
    ->get();

Daily Sales Difference

php
$sales = Sale::query()
    ->select('date', 'amount')
    ->movingDifference('amount', 'date', 'daily_change')
    ->orderBy('date')
    ->get();

Output:

dateamountdaily_change
2025-06-0110001000
2025-06-021200200
2025-06-03800-400

movingAverage()

Calculates a moving average over a specified window of rows using SQL's AVG() window function. Commonly used to smooth out short-term fluctuations and highlight longer-term trends.

What It Does

For each row in the result set, movingAverage() computes the average of the specified column using the current row and a defined number of preceding rows, based on the order column. The number of rows is determined by the $windowSize parameter.

SQL example generated:

sql
AVG(column) OVER (
    ORDER BY order_column
    ROWS BETWEEN N PRECEDING AND CURRENT ROW
) AS alias

Where N = windowSize - 1.

Use Cases

  • Smoothing sales data over weeks or months
  • Analyzing stock prices with trailing window averages
  • Tracking temperature or energy use in environmental logs
  • Monitoring trends in any sequential, numeric dataset

Calculate weekly moving average of daily sales

php
Sale::query()
    ->select('date', 'amount')
    ->movingAverage('amount', 7, 'date', 'weekly_avg')
    ->orderBy('date')
    ->get();

Adds a weekly_avg column showing a rolling average of the last 7 days' sales (including the current day).

30-Day Moving Average of Stock Prices

php
StockPrice::query()
    ->select('date', 'closing_price')
    ->movingAverage('closing_price', 30, 'date', 'monthly_avg')
    ->orderBy('date')
    ->get();

Adds monthly_avg showing the average closing price over the last 30 days per row.

useRaw()

The useRaw() method allows you to run raw SQL queries with parameter bindings, which helps prevent SQL injection by safely binding parameters to the query.

php
User::query()->useRaw(
    'SELECT * FROM user WHERE created_at > ? AND status = ?',
    ['2023-01-01', 'active']
);

useRaw runs a custom SQL query that retrieves users created after January 1, 2023, and with an active status. The values 2023-01-01 and active are securely bound to the query to prevent SQL injection.

Query Filter Using match()

The match() method in Doppar offers a clean and expressive way to apply dynamic, reusable filters to your queries. It supports both simple key-value pairs and more complex conditions using callbacks, making it ideal for building powerful, customizable queries without cluttering your controller or model logic.

Basic Filtering

php
Post::match([
    'id' => 1,
    'user_id' => 1
])->get();

Applies standard where clauses for each key-value pair.

Advanced Filtering with Callback

php
Post::match(function ($query) {
    $query->where('views', '>', 100)
          ->whereBetween('created_at', ['2023-01-01', '2023-12-31']);
})->get();

Allows complex conditions inside a closure for more flexible logic.

Filter Using Request Data

php
Post::match($request->only(['title', 'user_id']))->paginate();

Automatically filters based on available request fields—great for search forms or filters.

Combining Simple and Complex Filters

php
Post::match([
    'user_id' => [1, 2, 3],
    'created_at' => null,
    'active' => function ($query) {
        $query->where('active', 1)
              ->orWhere('legacy', 1);
    }
])
->orderBy('created_at', 'desc')
->get();

The match() method enhances query readability and reduces repetitive condition-building, especially in service layers or controllers.

if() for Conditional Query Execution

In Doppar, the if() method provides a powerful and elegant way to conditionally add query constraints based on a given condition. This allows you to build more flexible and dynamic queries, improving code readability and reducing unnecessary complexity.

Doppar ORM's if() method allows you to conditionally add query constraints based on a given condition. If the condition evaluates to true, the corresponding query modification is applied; otherwise, it is skipped.

Basic Usage of if()

php
Post::query()
    ->if($request->has('date_range'), function($query) use ($request) {
        $query->whereBetween('created_at', [
            $request->input('date_range.start'),
            $request->input('date_range.end')
        ]);
    })
    ->get();

In this example, the if() method checks if the date_range is present in the request. If it is, the query will filter the posts based on the provided start and end dates. If not, the whereBetween() condition is skipped.

Conditional Query Modifications with a Default Case

You can also provide a default case to be applied when the condition is false or not provided:

php
Post::query()
    ->if($request->input('search'),
        fn($q) => $q->where('title', 'LIKE', "%{$request->search}%"), // If search is provided, filter by title
        fn($q) => $q->where('is_featured', '=', true) // If no search is provided, filter by featured status
    )
    ->get();

In this example:

  • If a search parameter is provided, the query will search posts by title.
  • If no search parameter is found, it defaults to filtering posts where is_featured is true.

How if() Works with Different Conditions

Will execute when the condition is truthy:

php
Post::query()
    ->if(true, fn($q) => $q->where('active', '=', true)) // Executes because true
    ->if('text', fn($q) => $q->where('title', '=', 'text')) // Executes because 'text' is truthy
    ->if(1, fn($q) => $q->where('views', '=', 1)) // Executes because 1 is truthy
    ->if([1], fn($q) => $q->whereIn('id', '=', [1])) // Executes because [1] is truthy
    ->get();

Will NOT execute when the condition is falsy:

php
Post::query()
    ->if(false, fn($q) => $q->where('active', '=', false)) // Does not execute because false
    ->if(0, fn($q) => $q->where('views', '=', 0)) // Does not execute because 0 is falsy
    ->if('', fn($q) => $q->where('title', '=', '')) // Does not execute because empty string is falsy
    ->if(null, fn($q) => $q->where('deleted_at', '=', null)) // Does not execute because null is falsy
    ->if([], fn($q) => $q->whereIn('id', '=',  [])) // Does not execute because empty array is falsy
    ->get();

This makes the if() method powerful for dynamically building queries based on various conditions. It allows for more concise and flexible query building without having to manually check each condition before applying the relevant query changes.

Insertion and Update

Inserts

Of course, when using Doppar, working with data isn't limited to just retrieving records — inserting new entries is just as important. Fortunately, Doppar makes this process straightforward. To add a new record to the database, you simply create a new instance of the model, assign values to its attributes, and then call the save() method:

php
use App\Models\User;

$user = new User();
$user->name = 'Mahedi Hasan';
$user->email = 'mahedi@doppar.com';
$user->save();

This inserts a new row into the users table with the specified name and email. The save() method takes care of generating and executing the appropriate SQL insert query behind the scenes, making data insertion clean and intuitive.

Alternatively, you may use the create method to "save" a new model using a single PHP statement. The inserted model instance will be returned to you by the create method:

php
use App\Models\User;

User::create([
    'name' => 'Doppar'
]);

Updates

The save() method in Doppar isn't just for inserting new records—it can also be used to update existing ones. To update a record, first retrieve the model instance, modify the attributes you want to change, and then call save(). Doppar will handle generating the appropriate UPDATE statement. Additionally, the updated_at timestamp will be refreshed automatically, so you don't need to set it manually:

php
use App\Models\Flight;

$flight = Flight::find(1);
$flight->name = 'Dhaka to Mumbai';
$flight->save();

Occasionally, you may need to update an existing model. The updateOrCreate() method, the updateOrCreate() method persists the model, so there's no need to manually call the save method.

The updateOrCreate() method is used to either update an existing record or create a new one if no matching record is found. It simplifies handling scenarios where you need to ensure a record exists with specific attributes while updating other fields.

php
$user = User::updateOrCreate(
    ['email' => 'howdy@doppar.com'], // attributes to match
    ['name' => 'Doppar'] // values to update/create
);

Mass Updates

Updates can also be performed against models that match a given query. In this example, that are active will be marked as delayed:

php

User::query()
    ->where('country', '=', 'bd')
    ->update([
        'code' =>  '+880'
    ]);

Updating Data with fill()

In Doppar, you can streamline the process of updating multiple attributes by using the fill() method. This method allows you to pass an array of key-value pairs representing the fields you want to update. After filling the model with new data, you simply call save() to persist the changes:

php
$user = User::find(1);
$user->fill([
    'name' => 'John',
    'email' => 'updated@doppar.com',
]);
$user->save();

Bulk Insert Using saveMany()

When you need to insert multiple records at once, Doppar provides the saveMany() method, which simplifies batch inserts. Instead of saving each record individually, you can pass an array of data, and Doppar will insert them all in a single operation.

Here’s an example of how you can use saveMany() to insert multiple users:

php
User::saveMany([
    ['name' => 'John', 'email' => 'john@example.com', 'password' => bcrypt('password')],
    ['name' => 'Jane', 'email' => 'jane@example.com', 'password' => bcrypt('password')],
    ['name' => 'Bob', 'email' => 'bob@example.com', 'password' => bcrypt('password')]
]);

This inserts all three users into the users table in one batch, making the process more efficient than inserting them one by one.

Chunk Size for Large Datasets

If you are working with a large dataset, inserting all records at once might lead to performance issues or memory limitations. In such cases, you can specify a chunk size as the second parameter to saveMany() to batch the inserts into smaller chunks. For example:

php
User::saveMany([
    ['name' => 'John', 'email' => 'john@example.com', 'password' => bcrypt('password')],
    ['name' => 'Jane', 'email' => 'jane@example.com', 'password' => bcrypt('password')],
    ['name' => 'Bob', 'email' => 'bob@example.com', 'password' => bcrypt('password')]
], 1000);

This will insert the records in chunks of 1000, helping prevent memory overflow and improving performance when dealing with a large volume of data.

Using saveMany() is a great way to handle bulk inserts in Doppar, ensuring your application remains efficient even with large datasets.

Mass Assignment

You may use the create method to "save" a new model using a single PHP statement. The inserted model instance will be returned to you by the method:

php
use App\Models\User;

$user = User::create([
    'name' => 'Nure',
]);

However, before using the create method, you will need to specify a $creatable property on your model class. These properties are required because all Eloquent models are protected against mass assignment vulnerabilities by default.

php
<?php

namespace App\Models;

use Phaseolies\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $creatable = ['name'];
}

So, to get started, you should define which model attributes you want to make mass assignable. You may do this using the $creatable property on the model. For example, let's make the name attribute of our User model mass assignable:

Deleting Models

To delete a model, you may call the delete method on the model instance:

php
User::find(1)->delete();

Deleting an Existing Model by its Primary Key

In Doppar, if you already know the primary key(s) of the records you want to delete, there's no need to retrieve the models first. Instead, you can call the purge method directly. This method efficiently deletes records by their primary key and supports various input formats, including single IDs, multiple IDs, and arrays

php
User::query()->purge(1);
User::query()->purge(1, 2, 3);
User::query()->purge([1, 2, 3]);

Deleting Models Using Queries

Of course, Doppar also allows you to perform bulk deletions by building a query that targets specific records. For instance, you can delete all users that are marked as inactive in a single operation. Just like with mass updates, these mass deletions are executed directly in the database.

php
User::query()
    ->where('active', '=', false)
    ->delete();

Aggregation

The Doppar ORM provides a rich set of aggregation functions that allow you to perform statistical and summary operations directly through your model queries. These methods help extract meaningful insights from your data without writing raw SQL, making your code clean, expressive, and efficient.

Total Sum of a Column

To calculate the sum of all values in a specific column (e.g., total views across all posts):

php
Post::query()->sum('views');

Average Value of a Column

To compute the average value of a column (e.g., average number of views):

php
Post::query()->avg('views');

Maximum Value in a Column

To get the highest value in a column (e.g., the most expensive product):

php
Product::query()->max('price');

Minimum Value in a Column

To get the lowest value in a column (e.g., cheapest product):

php
Product::query()->min('price');

Standard Deviation Calculation

To compute the standard deviation of values in a column (useful for measuring variability):

php
Product::query()->stdDev('price');

Variance Calculation

To calculate the variance of values (a squared measure of data spread):

php
Product::query()->variance('price');

Multiple Aggregations in One Query

To retrieve count, average, min, and max in a single grouped query:

php
Product::query()
    ->select([
        'COUNT(*) as count',
        'AVG(price) as avg_price',
        'MIN(price) as min_price',
        'MAX(price) as max_price'
    ])
    ->groupBy('variants')
    ->first();

This groups data by variants and returns aggregated stats for each group.

Fetching Distinct Rows

To get unique values from a column (e.g., all unique user IDs who posted):

php
Post::query()->distinct('user_id');

Calculating Conditional Sum

To sum values under a specific condition (e.g., sales where title is "maiores"):

php
Product::query()
    ->where('title', '=', 'maiores')
    ->sum('price');

Total Sales by Category

To aggregate sales per category by multiplying price and quantity:

php
return Product::query()
    ->select(['category_id', 'SUM(price * quantity) as total_sales'])
    ->groupBy('category_id')
    ->get();

Column Modification: increment() & decrement()

Incrementing a Column

To increase the value of a numeric column (e.g., views):

php
$post = Post::find(1);
$post->increment('views'); // Increments by 1 by default
$post->increment('views', 10); // Increments by 10

You can also update additional fields during the increment:

php
$post->increment('views', 1, [
    'updated_at' => date('Y-m-d H:i:s'),
    'modified_by' => Auth::id()
]);

Decrementing a Column

To decrease the value of a numeric column:

php
$post = Post::find(1);
$post->decrement('views'); // Decrements by 1 by default
$post->decrement('views', 10); // Decrements by 10

You can also attach extra updates when decrementing:

php
$post->decrement('views', 1, [
    'updated_at' => date('Y-m-d H:i:s'),
    'modified_by' => Auth::id()
]);

Doppar ORM empowers you to run comprehensive statistical queries, manage data updates, and build analytics-driven features with minimal effort. Whether you're building dashboards, reports, or tracking metrics, these aggregation tools are essential for maintaining efficient and elegant code.

Transform Eloquent Collection

The Doppar ORM offers expressive methods like map(), filter(), and each() to transform, filter, and iterate over Eloquent collections with ease. These methods provide fine control over the shape and content of your data after fetching it from the database.

map() – Transforming Collection Items

Use the map() method to transform each item in a collection. This is especially useful when you want to reformat or limit the fields returned in your response.

Example: Return Only the name of Each User

php
return User::all()
    ->map(function ($item) {
        return [
            'name' => $item->name
        ];
    });

Use case: Customize API responses or prepare data for front-end consumption by trimming down unnecessary fields.

filter() – Conditional Filtering

After transforming a collection, you can use filter() to return only items that match a given condition.

Example: Return Posts Where status = 1, With Only title and status

php
return Post::all()
    ->map(function ($item) {
        return [
            'title' => $item->title,
            'status' => $item->status
        ];
    })
    ->filter(function ($item) {
        return $item['status'] === 1;
    });

each() – Iterating Through Items Without Changing Structure

The each() method is used to perform actions on each item in the collection without modifying the collection itself. It's great for side effects like logging, notifications, or conditional updates.

Example: Run Logic on Each Latest Post

php
return Post::query()->newest()->get()
    ->each(function ($post) {
        // Perform some operation on each post
    });

Summary of Collection Utilities

MethodPurposeMutates Collection?
map()Transforms each item✅ Yes
filter()Filters items by a condition✅ Yes
each()Iterates for side-effects (no return)❌ No

These methods make Doppar’s Eloquent collection manipulation concise, readable, and highly adaptable for API responses, data formatting, and conditional logic.

Pagination

Pagination is an essential feature for working with large datasets, enabling you to fetch and display data in manageable chunks. This improves both performance and user experience, especially in applications that deal with lists like users, posts, products, or logs.

Doppar makes pagination seamless through the built-in paginate() method, which handles all the behind-the-scenes logic for splitting your results into pages.

Basic Usage

To paginate a dataset, simply call the paginate() method like as follows:

php
User::query()->paginate(1);

This will give you the response like this

json
{
    "data": [
        {
            "id": 4,
            "name": "Caden Jerde",
            "email": "stokes.ludwig@hotmail.com",
            "created_at": "2025-05-06 09:59:57",
            "updated_at": "2025-05-06 09:59:57"
        }
    ],
    "first_page_url": "http://example.com/users?page=1",
    "last_page_url": "http://example.com/users?page=21",
    "next_page_url": "http://example.com/users?page=2",
    "previous_page_url": null,
    "path": "http://example.com/users",
    "from": 1,
    "to": 1,
    "total": 21,
    "per_page": 1,
    "current_page": 1,
    "last_page": 21
}

Display Pagination in Views

When working with paginated data in your views, Doppar provides two convenient methods to render pagination links. These methods allow you to display navigation controls for moving between pages, ensuring a smooth user experience.

Available Methods:

  • linkWithJumps() method generates pagination links with additional "jump" options, such as dropdown with paging. It is ideal for datasets with a large number of pages, as it allows users to quickly navigate to the beginning or end of the paginated results.
  • links() This method generates standard pagination links, including "Previous" and "Next" buttons, along with page numbers. It is suitable for most use cases and provides a clean and simple navigation interface.

Now call the pagination for views

html
@foreach ($data['data'] as $user)
    <tr>
        <td>{{ $user->id }}</td>
        <td>{{ $user->name }}</td>
        <td>{{ $user->username }}</td>
        <td>{{ $user->email }}</td>
    </tr>
@endforeach

<!-- "Previous" and "Next" buttons, along with page numbers. -->
{!! paginator($data)->links() !!}

<!-- "Previous" and "Next" buttons, along with page jump options. -->
{!! paginator($data)->linkWithJumps() !!} //

Customize Default Pagination

Doppar provides a Bootstrap 5 pagination view by default. However, you can also customize this view to suit your needs. To customize the pagination view, Doppar offers the publish:pagination pool command. Running this command will create two files, jump.blade.php and number.blade.php, inside the resources/views/vendor/pagination folder. These files allow you to tailor the pagination design to match your application's style.

bash
php pool publish:pagination

Once you modify the jump.blade.php and number.blade.php files, the changes will immediately reflect in your pagination view. This allows you to fully customize the appearance and behavior of the pagination links to align with your application's design and requirements. Feel free to update these files as needed to create a seamless and visually consistent user experience.

Retrieving a Paginated Subset of Records

To fetch a specific subset of records from the database—such as a “page” of users—you can use the offset and limit methods on an Eloquent query.

php
use App\Models\User;

$users = User::query()
    ->offset(4)  // Skip the first 4 records
    ->limit(10)  // Retrieve the next 10 records
    ->get();

return $users;

This is useful for simple pagination or when you want to retrieve a specific slice of your dataset.

Database Transactions

A database transaction is a sequence of database operations that are executed as a single unit. Transactions ensure data integrity by following the ACID properties (Atomicity, Consistency, Isolation, Durability). If any operation within the transaction fails, the entire transaction is rolled back, preventing partial updates that could leave the database in an inconsistent state.

Doppar provides built-in support for handling database transactions using the DB::transaction() method, DB::beginTransaction(), DB::commit(), and DB::rollBack().

Using DB::transaction() for Simplicity

The DB::transaction() method automatically handles committing the transaction if no exception occurs and rolls it back if an exception is thrown.

php
DB::transaction(function () {
    $user = User::create($request->all());
    $post = Post::create([
        'user_id' => $user->id, 
        'title' => 'War is started'
    ]);
});

Manually Handling Transactions

In cases where more control is needed, transactions can be manually started using DB::beginTransaction(). The operations must then be explicitly committed or rolled back.

php
DB::beginTransaction();
try {
    $user = User::create([
        'name' => 'Mahedi',
        'email' => fake()->email,
        'password' => bcrypt('password'),
    ]);

    Post::create([
        'title' => 'My very first post',
        'user_id' => $user->id
    ]);

    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    throw $e;
}

Handling Deadlocks with Transaction Retries

Deadlocks can occur when multiple transactions compete for the same database resources. Doppar allows setting a retry limit for transactions using a second parameter in DB::transaction().

php
DB::transaction(function () {
    // Operations that might deadlock
}, 3); // Will attempt up to 3 times before throwing an exception

This approach helps mitigate issues caused by deadlocks by retrying the transaction a set number of times before ultimately failing.

Using transactions properly ensures database consistency and prevents data corruption due to incomplete operations. Doppar provides flexible methods for handling transactions, allowing both automatic and manual control based on the use case.

Eloquent Join

In Doppar, Eloquent manual joins allow you to retrieve data from multiple tables based on a related column. The join method in Eloquent's Query Builder provides an easy way to combine records from different tables. This document explains how to perform various types of joins manually using Eloquent's Eloquent ORM and Query Builder.

Basic Join Example

A simple join operation can be performed using the join method to combine records from two tables based on a common key. Below is an example of joining users and posts tables:

php
$users = User::query()
    ->select('posts.*', 'users.name as user_name')
    ->join('posts', 'users.id', '=', 'posts.user_id')
    ->get();

This will return a dataset containing user data which has at least one post.

Specifying Join Type

By default, Doppar performs an INNER JOIN. You can specify the type of join you want by passing the join type as an argument:

php
$users = User::query()
    ->join('posts', 'users.id', '=', 'posts.user_id', 'left')
    ->get();

Here, a LEFT JOIN is used to include all users, even if they do not have associated posts.

Applying Conditions in Joins

You can apply additional conditions to filter the joined data. The example below joins the posts table and fetches only the users who have published posts:

php
$users = User::query()
    ->join('posts', 'users.id', '=', 'posts.user_id')
    ->where('posts.published', '=', true)
    ->orderBy('users.name', 'ASC')
    ->get();

Performing Multiple Joins

You can join multiple tables in a single query. The example below joins the users, posts, and comments tables:

php
$users = User::query()
    ->join('posts', 'users.id', '=', 'posts.user_id')
    ->join('comments', 'posts.id', '=', 'comments.post_id')
    ->get();

This will return data containing users, their posts, and associated comments.

Selecting Specific Columns

To optimize queries and improve performance, you can select specific columns instead of retrieving all fields:

php
$users = User::query()
    ->select('users.name', 'posts.title')
    ->join('posts', 'users.id', '=', 'posts.user_id')
    ->get();

This query retrieves only the users.name and posts.title fields, reducing the amount of data transferred.

DB Query

Doppar’s DB Facade, available via the Phaseolies\Support\Facades\DB namespace, provides a clean, expressive, and powerful interface to interact directly with your database—going beyond the ORM layer when you need low-level control or raw SQL flexibility.

Whether you're running raw queries, managing transactions, executing stored procedures, or querying database views, Doppar gives you a full toolkit under the DB facade.

Basic Database Utilities

OperationExampleDescription
Get all tablesDB::getTables()Returns an array of all table names in the database
Check table existenceDB::tableExists('user')Returns true if the specified table exists
Get table columnsDB::getTableColumns('user')Returns a list of column names from a table
Get table from modelDB::getTable(new User())Fetches the associated table name of a model
Get raw PDO connectionDB::getConnection()Returns the underlying PDO object

Querying the Database with query()

The query() method provided by Doppar enables you to execute raw SQL statements with parameter binding to ensure safe and secure queries. This method returns a \Phaseolies\Support\Collection giving you full control over result handling.

The following example demonstrates how to execute a parameterized SQL query and retrieve the first matching row as an associative array:

php
<?php

use Phaseolies\Support\Facades\DB

$user = DB::query("SELECT * FROM users WHERE name = ?", ['Caden Jerde']);
$user->name;

This will return the first row where the name column matches "Kasper Snider", or null if no such row exists.

Get All Rows

In this example, all rows from the users table are retrieved and stored in the $users collection. If there are no matching rows, an empty array will be returned.

php
$users = DB::query("SELECT * FROM users");

The query() method returns a Collection, allowing you to fluently chain collection methods like map(), filter(), and others. This makes it easy to transform or manipulate the result set directly after querying the database.

If you expect multiple rows, you can use the map() method to transform each row into a custom structure or format.

php
<?php

use Phaseolies\Support\Facades\DB;

return DB::query("SELECT * FROM users")
    ->map(function ($data) {
        return [
            'name' => $data['name'] ?? 'default',
        ];
    });

Modifying the Database using execute()

Doppar provides execute() function and using it, you can modify database. See the basic example. Inserts a new user record. Returns the number of affected rows (1 if successful).

php
$inserted = DB::execute(
    "INSERT INTO users (name, email, password) VALUES (?, ?, ?)",
    ['John Doe', 'john@example.com', bcrypt('secret')]
);

You can use transaction here as well

php
DB::transaction(function () {
    $moved = DB::execute(
        "INSERT INTO archived_posts SELECT * FROM posts WHERE created_at < ?",
        [date('Y-m-d', strtotime('-1 year'))]
    );

    $deleted = DB::execute(
        "DELETE FROM posts WHERE created_at < ?",
        [date('Y-m-d', strtotime('-1 year'))]
    );

    echo "Archived {$moved} posts and deleted {$deleted} originals";
});

Executes multiple statements in a single transaction. Ensures either all queries succeed or none are committed.

Executing Stored Procedures

Doppar provides support for executing stored procedures through the procedure() method. This allows you to call database-stored routines directly, making it ideal for complex logic encapsulated within the database.

By default, procedure() returns a \Phaseolies\Support\Collection results.

php
$nestedResults = DB::procedure('sp_GetAllUsers')->all();

This will return the all users collection data.

Get first row

When you only need the first row from the first result set of a stored procedure, you can use the first() method after calling procedure(). This is useful for cases where the procedure returns a list, but you only care about the top result.

php
$firstUser = DB::procedure('sp_GetAllUsers')->first();

// with passing parameter
$firstUser = DB::executeProcedure('sp_GetAllUsers', [1])->first();

Get last row

When you only need the last row from the first result set of a stored procedure, you can use the last() method after calling procedure(). This is useful for cases where the procedure returns a list, but you only care about the last result.

php
$firstUser = DB::procedure('sp_GetAllUsers')->last();

Get the second result set (index 1) by passing 123 parameter

php
$stats = DB::procedure('sp_GetUserWithStats', [123])->resultSet(1);

With Multiple Params

Doppar's procedure() method supports stored procedures that return multiple result sets. The returned result object offers convenient methods to navigate and extract specific parts of the output.

php
$result = Database::procedure('get_multiple_results');
$result->resultSet(0); // First set
$result->resultSet(1); // Second set

// Get last row of first result set
$lastRow = $result->last();

// Get last row of second result set
$lastRowSet2 = $result->lastSet(1);

Executing View

Doppar makes it easy to query database views using the view() method. A view behaves like a virtual table and can be queried just like a regular table, often used to simplify complex joins or aggregations.

php
$stats = DB::view('vw_user_statistics');

View with WHERE Conditions

You can pass where condition in your custom view like

php
// View with single WHERE condition
$nyUsers = DB::view(
    'vw_user_locations', 
    ['state' => 'New York']
);

// Equivalent to: SELECT * FROM vw_user_locations WHERE state = 'New York'

// View with multiple WHERE conditions
$premiumNyUsers = DB::view(
    'vw_user_locations',
    [
        'state' => 'New York',
        'account_type' => 'premium'
    ]
);

// Equivalent to: 
// SELECT * FROM vw_user_locations 
// WHERE state = 'New York' AND account_type = 'premium'

View with Parameter Binding

You can also pass params with where condition as follows

php
// 4. Using parameter binding for security
$recentOrders = DB::view(
    'vw_recent_orders',
    ['status' => 'completed'],
    [':min_amount' => 100] // Additional parameters
);

// Equivalent to:
// SELECT * FROM vw_recent_orders 
// WHERE status = 'completed' AND amount > :min_amount

Summery

FeatureMethodPurpose
Raw Queriesquery()Run SELECT statements with parameter binding
Data Modificationexecute()Run INSERT, UPDATE, DELETE
Transactionstransaction()Execute multiple queries safely
Stored Proceduresprocedure()Call stored procs with multiple results
Viewsview()Query DB views with filters and bindings
UtilitiesgetTables(), tableExists()Explore schema

Doppar's DB Facade provides all the raw SQL power you need while preserving the developer-friendly syntax you love.

Handling Large Dataset

Working with large datasets can be challenging in terms of performance and memory efficiency. The Doppar framework provides a suite of tools within the \Phaseolies\Database\Eloquent\Query\QueryProcessor trait to help you process and stream large amounts of data without exhausting system resources. These tools are especially useful for batch operations, background jobs, reporting systems, and data migrations.

This module introduces a variety of data handling strategies tailored to different use cases, including:

  • Chunk-based processing to break large datasets into manageable segments.
  • Cursor-based streaming for low-memory iteration over millions of records.
  • Generators to provide lazy, memory-efficient traversal of datasets.
  • Fiber-based concurrency for advanced parallel data processing and streaming.

All these approaches are designed with memory safety in mind, incorporating garbage collection and careful resource cleanup to avoid memory leaks or excessive overhead during execution.

Whether you're building scalable ETL pipelines or simply need to process records in a loop without crashing your application, Doppar’s dataset handling capabilities provide a robust and flexible foundation.

chunk

When dealing with large datasets, loading all records into memory at once can cause significant performance degradation or even out-of-memory errors. The chunk() method addresses this by breaking the dataset into smaller chunks and processing each chunk independently. This allows you to iterate through the entire dataset efficiently, without overwhelming system resources.

The chunk() method accepts three parameters:

  • $chunkSize – the number of records to retrieve per batch.
  • $processor – a callback function that receives each chunk for processing.
  • $total (optional) – the total number of records expected, useful for tracking progress.

Each chunk is fetched using pagination via LIMIT and OFFSET. After processing a chunk, the memory is freed, and garbage collection is invoked to minimize leaks.

Below is an example where we process active users in chunks of 100 records:

php
use App\Models\User;
use Phaseolies\Support\Collection;

User::query()
    ->where('status', '=', true)
    ->chunk(100, function (Collection $users) {
        foreach ($users as $user) {
            // Perform operations on each user
        }
    });

This approach keeps memory usage low and is ideal for operations such as batch updates, exporting data, or applying transformations to large tables.

Tracking Progress with chunk()

In addition to memory-efficient processing, the chunk() method in Doppar also supports progress tracking. This is especially useful when you want to provide feedback during long-running operations, such as logging progress, displaying status bars, or estimating time to completion.

By passing a third parameter, $total, to the chunk() method, your processor callback will receive not only the current chunk of data but also:

php
use App\Models\User;
use Phaseolies\Support\Collection;

User::query()
    ->where('status', '=', true)
    ->chunk(
        chunkSize: 100,
        processor: function (Collection $users, $processed, $total) {
            foreach ($users as $user) {
                // Perform operations on each user
                echo "Processed $processed of $total\n";
            }
        },
        total: 500
    );

This pattern is ideal when you're running background tasks or CLI scripts and want real-time feedback or logging.

fchunk() – Concurrent Chunk

When speed is critical and your system can handle parallel operations, Doppar provides the fchunk() method—a fiber-based parallel chunk processor. This allows multiple chunks to be processed concurrently, taking advantage of lightweight PHP Fibers to speed up the total processing time.

Unlike traditional chunk() processing, which is sequential, fchunk() runs multiple chunks in parallel, each in its own fiber. This can significantly reduce the time needed to process large datasets, especially when the per-record logic is I/O-bound or computationally light.

Parameters

  • chunkSize: Number of records per chunk.
  • processor: Callback to process each chunk of records.
  • concurrency: Number of fibers (chunks) to process in parallel.

⚠️ Note: Fiber-based concurrency is cooperative, not true multithreading. It still runs on a single PHP thread, but enables interleaving tasks efficiently.

Example wuth concurrently processing active users

php
use App\Models\User;
use Phaseolies\Support\Collection;

User::query()
    ->where('status', '=', true)
    ->fchunk(
        chunkSize: 100,
        processor: function (Collection $users) {
            foreach ($users as $user) {
                // Handle user logic
            }
        },
        concurrency: 4
    );

In this example:

  • Users are fetched in chunks of 100.
  • Up to 4 chunks are processed in parallel
  • Each chunk is passed to the callback where individual records are handled.

This technique is ideal for high-throughput batch jobs where database or API operations can be overlapped efficiently. It's especially helpful in CLI scripts or daemons where time-to-completion matters.

cursor() – Stream Records

The cursor() method is designed for ultra-efficient, low-memory iteration over large datasets. Instead of loading all records or chunks into memory, cursor() uses a forward-only PDO cursor to stream records one at a time directly from the database.

This method is ideal when dealing with millions of records, performing exports, or executing operations where memory usage must stay constant.

Parameters

  • processor: A callback function that is invoked for each record.
  • total (optional): Total number of records expected, useful for progress tracking.

How It Works

Internally, cursor() prepares the query, binds parameters, and fetches each row as an associative array. Each row is immediately converted into a model and passed to the callback function. After each record is processed, the memory is cleaned up and garbage collection is triggered.

php
use App\Models\User;

User::query()
    ->where('status', '=', true)
    ->cursor(function ($user) {
        // Handle user logic
    });

The operation is memory-stable regardless of dataset size.

  • Best for: Iterating huge datasets with low memory usage.

⚠️ Avoid using cursor() if your processing logic requires sorting, seeking, or buffering large sets of rows.

You can also handle progress in cursor like this way

php
User::query()
    ->where('status', '=', true)
    ->cursor(function ($user, $processed, $total) {
        //  Handle user logic
        echo "Processed $processed of $total\n";
    }, 500);

fcursor() – Hybrid Cursor

The fcursor() method blends the memory efficiency of a traditional cursor with the performance benefits of Fibers. This hybrid approach lets you stream records from the database in batches using a cursor, but buffer and process them efficiently using Fiber-based suspension.

Unlike cursor() which processes one row at a time, fcursor() buffers a group of records (using a configurable bufferSize) and yields them together to the processor. This reduces the overhead of per-row context switching and provides a balance between memory usage and performance.

Parameters

  • processor: A callback function invoked for each record in the buffered batch.
  • bufferSize: The number of records to accumulate before passing them to the processor (default is 1000).

How It Works

  • A PDO cursor fetches records from the database.
  • Records are buffered into an array.
  • Once the buffer reaches the defined bufferSize, the Fiber suspends and yields control to the processor.
  • Processing resumes after the callback is done, and continues until all records are exhausted.

Example with user list streaming

php
use App\Models\User;

User::query()
    ->where('status', '=', true)
    ->fcursor(function ($user) {
        //
    });

This method is particularly useful when:

  • You want better performance than single-row cursors.
  • Your logic can handle medium-sized memory buffers.
  • You’re building CLI tools or background workers that must process large volumes reliably.

⚡ Efficient like cursor(), but faster for many real-world workloads due to fiber-based batching.

stream() – Lazy Generator

The stream() method offers a clean, generator-based approach to iterating over large datasets in a memory-efficient way. It loads records in small chunks (like chunk()), but yields one record at a time, allowing you to use it with foreach just like a native PHP array — without holding everything in memory.

Unlike cursor(), stream() handles records in small batches (e.g., 100 at a time) and is suitable when you prefer chunked I/O with on-the-fly transformation or mapping.

Parameters

  • chunkSize: The number of records to fetch per internal chunk.
  • transform (optional): A callback function to transform each model before yielding.

Example with Lazy Streaming

php
use App\Models\User;

$users = User::query()
    ->where('status', '=', true)
    ->stream(100); // Fetch 100 at a time lazily

foreach ($users as $user) {
    //
}

In this example Each user is yielded individually from the generator. Memory usage stays consistent regardless of dataset size.

Transforming Models During Stream

You can also transform records before they are yielded using the optional transform parameter.

php
use App\Models\User;

foreach (
    User::query()
        ->where('status', '=', true)
        ->stream(1000, function ($user) {
            return strtoupper($user->name); // Convert name to uppercase
        })
    as $userName
) {
    dump($userName); // Already transformed string
}

In this example

  • Here, each user is mapped to an uppercase string before being yielded.
  • The result is a generator of transformed values (not models).
  • Useful for lightweight export pipelines, CSV dumps, or messaging queues.

💡 Tip: Use stream() when you want a foreach-friendly syntax with low memory use, especially in long-running CLI tasks or APIs.

fstream() – Streaming with Backpressure Control

The fstream() method is an advanced Fiber-powered generator for streaming large datasets efficiently. It combines the lazy iteration style of stream() with the performance and concurrency control of Fibers.

Unlike stream(), fstream() is designed for high-throughput environments where you may want to buffer records in controlled amounts and suspend/resume execution more precisely.

Parameters

  • chunkSize: Number of records to fetch per database request.
  • transform (optional): Callback to transform each model before yielding.
  • bufferSize: Maximum number of items to buffer before yielding (defaults to 1000).

Example: Streaming with Transformation

php
use App\Models\User;

foreach (
    User::query()
        ->where('status', '=', true)
        ->fstream(1000, function ($user) {
            return strtoupper($user->name); // Transform model to string
        })
    as $userName
) {
    dump($userName); // Already transformed
}

In this example

  • Records are fetched in chunks of 1000.
  • Transformed immediately into uppercase strings.
  • Buffer is filled (up to default 1000) before being flushed into the foreach loop.
  • Memory remains stable, and Fiber context switching keeps performance high.

How It Works

  • Internally, Fibers pull in small batches of records.
  • Each record is passed through the transform (if given).
  • Fibers suspend after each yield, keeping processing responsive.
  • A buffer size (default 1000) prevents overloads while maximizing I/O efficiency.

batch() – Grouped Batch Processing

The batch() method is tailored for scenarios where you want to process large numbers of records in grouped batches instead of one at a time. It provides an efficient way to collect data in memory-limited chunks and flush it to a processor in meaningful units (e.g., every 1000 records).

This is particularly useful when writing to external systems, doing bulk inserts, or when you want to limit the number of operations per database or API call.

Parameters

  • chunkSize: Number of records to fetch from the database per query.
  • batchProcessor: A callback that receives each batch as a Collection.
  • batchSize: Number of records per batch to accumulate before processing (default: 1000).

Example: Process Users in Batches of 1000

php
use App\Models\User;

User::query()
    ->where('status', '=', true)
    ->batch(
        chunkSize: 500,
        batchProcessor: function ($batch) {
            foreach ($batch as $user) {
                //
            }

            echo "Processed batch of {$batch->count()} users\n";
        },
        batchSize: 1000
    );

In this example

  • This example fetches records in chunks of 500 from the database.
  • Records are accumulated into a batch of 1000 before calling the processor.
  • The batchProcessor is only triggered once the batch size is met (or at the end of the stream).