Thanks to visit codestin.com
Credit goes to bowphp.com

Skip to main content
Version: 5.x

ORM Barry

Barry is the ORM (Object Relation Mapping) built into BowPHP.

Introduction​

info

An ORM (Object Relation Mapping) is a way of linking tables together using classes. Each record in a table represents an object that can be related to other records.

The ORM included with BowPHP provides a simple and elegant ActiveRecord implementation for working with your database. Each database table has a corresponding "model" that is used to interact with that table. Models allow you to query data in your tables, as well as insert new records.

In any self-respecting framework, there is an ORM system with a friendly name. Bow's is called Barry.

Prerequisites

Before you begin, make sure you have configured a database connection in config/database.php.

Before continuing, please add a migration:

php bow add:migration CreateTodoTable

Then edit the migration:

public function up()
{
$this->create("todos", function (Table $table) {
$table->addIncrement('id');
$table->addString('title');
$table->addInteger('status', ["default" => 1]);
$table->addInteger('budget', ["default" => 0]);
$table->addTimestamps();
});
}

Finally, run the migration:

php bow migration:migrate
Information

This migration will be used to let you run tests directly on the App\Models\Todo::class model.

Adding a model​

To add a model, use the php bow command line with the add:model command followed by the model name.

php bow add:model Todo

After the model is created, a file with the same name will be created β€” in our case Todo.php β€” at the root of the app/Models folder.

Here is an overview of the file:

namespace App\Models;

use Bow\Database\Barry\Model;

class Todo extends Model
{
//
}
Important

Before using the model, make sure you have configured your database.

Table name​

Note that we did not tell Barry which table to use for our Todo model. By convention, the plural "snake_case" name of the class will be used as the table name unless another name is explicitly specified.

You can manually specify a table name by defining a table property on your model:

namespace App\Models;

use Bow\Database\Barry\Model;

class Todo extends Model
{
/**
* Define the table associated with the model.
*/
protected string $table = 'todos';
}

You can also apply a per-model prefix (prepended to the table name) β€” useful for coexisting with other systems in the same database:

class Todo extends Model
{
protected string $prefix = 'app_';
protected string $table = 'todos'; // resolved as "app_todos"
}

Primary keys​

Barry will also assume that each table has a primary key column named id. You can define a protected $primary_key property to override this convention:

namespace App\Models;

use Bow\Database\Barry\Model;

class Todo extends Model
{
/**
* The primary key associated with the table.
*/
protected string $primary_key = 'id_todo';
}

You can also specify the type of the primary key and disable auto-increment (for example, for UUIDs or composite string identifiers):

class Todo extends Model
{
protected string $primary_key = 'uuid';
protected string $primary_key_type = 'string'; // 'int' (default) | 'string' | 'float' | 'double'
protected bool $auto_increment = false;
}

Connection​

By default, Barry uses the framework's current connection. To point a model at a specific connection, define the $connection property:

class Todo extends Model
{
protected ?string $connection = 'reporting';
}

You can also change the connection on a per-call basis:

$todos = Todo::connection('reporting')->all();

Retrieving data​

info

Once you have created a model and its associated database table, you are ready to start retrieving data. Think of each Barry model as a powerful query builder that lets you query the database table associated with the model.

For example:

use App\Models\Todo;

$todos = Todo::all();

foreach ($todos as $todo) {
echo $todo->title;
}

The retrieve and retrieveBy methods can also be used to retrieve information:

// With retrieve
$todo = Todo::retrieve(1);

// With retrieveBy (returns a Collection)
$todos = Todo::retrieveBy('status', 'pending');

You can also use retrieveOrFail to throw an exception if the record does not exist:

use Bow\Database\Exception\NotFoundException;

try {
$todo = Todo::retrieveOrFail(1);
} catch (NotFoundException $e) {
// The record does not exist
}
Note

The retrieve method can also return null when no record is found.

To get the most recently created record according to the column declared in $latest (created_at by default), use the static latest() method:

$todo = Todo::latest(); // ORDER BY created_at DESC LIMIT 1

// Customize the column used:
class Todo extends Model
{
protected string $latest = 'updated_at';
}

Adding additional constraints​

Barry's all method will return all results in the model's array. Since each Barry model serves as a query builder, you can also add constraints to queries, then use the get method to retrieve the results:

$flights = App\Models\Todo::where('status', 'done')
->orderBy('title', 'desc')
->take(10)
->get();

Query Builder methods​

Thanks to the magic __callStatic method, all Barry models have access to the Query Builder methods. These methods can be chained to build complex queries.

WHERE conditions​

use App\Models\Todo;

// Simple condition
Todo::where('status', 'done')->get();

// With comparison operator
Todo::where('budget', '>', 1000)->get();

// OR condition
Todo::where('status', 'done')
->orWhere('status', 'pending')
->get();

// WHERE with NULL value
Todo::whereNull('deleted_at')->get();
Todo::whereNotNull('completed_at')->get();

// WHERE BETWEEN
Todo::whereBetween('budget', [100, 500])->get();
Todo::whereNotBetween('budget', [100, 500])->get();

// WHERE IN
Todo::whereIn('status', ['done', 'pending'])->get();
Todo::whereNotIn('status', ['cancelled', 'expired'])->get();

// WHERE RAW (raw query)
Todo::whereRaw('budget > 100 AND status = "done"')->get();
Todo::orWhereRaw('created_at > NOW() - INTERVAL 7 DAY')->get();

Sorting and limiting​

use App\Models\Todo;

// Sort by column
Todo::orderBy('created_at', 'desc')->get();
Todo::orderBy('title', 'asc')->get();

// Limit the results
Todo::take(10)->get();

// Skip records (offset)
Todo::jump(5)->take(10)->get();

// Get the first record
Todo::where('status', 'done')->first();

// Get the last record
Todo::where('status', 'done')->last();

Column selection​

use App\Models\Todo;

// Select specific columns
Todo::select(['id', 'title', 'status'])->get();

// Distinct values
Todo::distinct('status');

Grouping​

use App\Models\Todo;

// GROUP BY
Todo::groupBy('status')->get();

// GROUP BY with HAVING
Todo::groupBy('status')
->having('count', '>', 5)
->get();

Joins​

use App\Models\Todo;

// INNER JOIN
Todo::join('users', 'todos.user_id', '=', 'users.id')->get();

// LEFT JOIN
Todo::leftJoin('categories', 'todos.category_id', '=', 'categories.id')->get();

// RIGHT JOIN
Todo::rightJoin('projects', 'todos.project_id', '=', 'projects.id')->get();

// Multiple joins with AND ON / OR ON
Todo::join('users', 'todos.user_id', '=', 'users.id')
->andOn('todos.team_id', '=', 'users.team_id')
->orOn('todos.owner_id', '=', 'users.id')
->get();

Write operations​

use App\Models\Todo;

// Update with conditions
Todo::where('status', 'pending')
->update(['status' => 'done']);

// Delete with conditions
Todo::where('status', 'cancelled')->delete();

// Increment a value
Todo::where('id', 1)->increment('view_count');
Todo::where('id', 1)->increment('view_count', 5); // +5

// Decrement a value
Todo::where('id', 1)->decrement('stock');
Todo::where('id', 1)->decrement('stock', 3); // -3

// Direct insert
Todo::insert([
'title' => 'New task',
'status' => 'pending'
]);

// Insert and retrieve the ID
$id = Todo::insertAndGetLastId([
'title' => 'New task',
'status' => 'pending'
]);

Existence check​

use App\Models\Todo;

// Check whether records exist
$exists = Todo::where('status', 'done')->exists();

// Check by column and value
$exists = Todo::exists('email', '[email protected]');

SQL generation​

use App\Models\Todo;

// Get the generated SQL query
$sql = Todo::where('status', 'done')
->orderBy('created_at', 'desc')
->toSql();
Going further

This section covers the most common Query Builder methods. For complete documentation with all the advanced options, see the Query Builder page.

Retrieving aggregates​

You can also use the count, sum, max, and other aggregation methods provided by the query builder. These methods return the appropriate scalar value instead of a full model instance:

use App\Models\Todo;

$count = Todo::where('status', 'done')->count();

$max = Todo::where('status', 'done')->max('budget');

Inserting and updating models​

INSERT​

To create a new record in the database, create a new model instance, set attributes on the model, then call the save method:


namespace App\Http\Controllers;

use App\Models\Todo;
use Bow\Http\Request;

class TodoController
{
/**
* Create a new todo instance.
*
* @param Request $request
* @return mixed
*/
public function store(Request $request)
{
// Validate the request...

$todo = new Todo;

$todo->title = $request->get('title');
$todo->budget = $request->get('budget', 0);
$todo->status = 'pending';

$todo->persist();
}
}

In this example, we assign the name parameter from the incoming HTTP request to the title and budget attributes of the App\Models\Todo model instance. When we call the persist method, a record will be inserted into the database. The created_at and updated_at timestamps will be set automatically when the persist method is called, so there is no need to set them manually.

Insert via CREATE​

Active Record objects can be created from a hash, from a block, or have their attributes set manually after creation. The new method will return a new object, while create will return the object and save it to the database.

For example, given a model user with name and occupation attributes, calling the create method will create and save a new record in the database:

use App\Models\Todo;

$user = Todo::create([
'title' => 'Buy a subway ticket',
'budget' => 2000,
'status' => 'pending',
]);

UPDATE​

The persist method can also be used to update models that already exist in the database. To update a model, you must retrieve it, set the attributes you want to update, then call the persist method. Again, the updated_at timestamp will be updated automatically, so there is no need to set its value manually:

use App\Models\Todo;

$todo = Todo::retrieve(1);

$todo->title = 'Shopping for Franck';

$todo->persist();

You can also use the update method. However, you must set conditions to limit the impact of the update.

use App\Models\Todo;

Todo::where('status', 'done')
->update(['title' => 'Buy a plane ticket']);

The update method expects an array of column-value pairs representing the columns to update.

Deleting data​

Likewise, once retrieved, an Active Record object can be destroyed, which removes it from the database.

use App\Models\Todo;

$todo = Todo::retrieve(1);
$todo->delete();

If you want to delete multiple records in bulk, you can use the deleteBy or truncate method:

// Delete a todo by column
Todo::deleteBy('status', 'done');

// Delete all todos
Todo::truncate();

Relationships between models​

Barry provides four types of relationships exposed via the Bow\Database\Barry\Concerns\Relationship trait (included by default in the Model class): hasOne, hasMany, belongsTo, and belongsToMany.

You declare a relationship as a method on the model that returns the relationship object. Accessing this method as a property triggers resolution and returns the results.

hasOne β€” One to one​

A user has one profile:

namespace App\Models;

use Bow\Database\Barry\Model;
use Bow\Database\Barry\Relations\HasOne;

class User extends Model
{
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
}

// Usage
$profile = User::retrieve(1)->profile; // β†’ Profile instance

hasMany β€” One to many​

A user has multiple posts:

use Bow\Database\Barry\Relations\HasMany;

class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
}

$posts = User::retrieve(1)->posts; // β†’ Collection of Post

belongsTo β€” Inverse of hasOne / hasMany​

A post belongs to a user:

use Bow\Database\Barry\Relations\BelongsTo;

class Post extends Model
{
public function author(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

$user = Post::retrieve(42)->author; // β†’ User instance

belongsToMany β€” Many to many​

A post has multiple tags via a pivot table:

use Bow\Database\Barry\Relations\BelongsToMany;

class Post extends Model
{
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class);
}
}

$tags = Post::retrieve(1)->tags; // β†’ Collection of Tag

Customizing the keys​

All methods accept two optional parameters to specify the join columns. The default names are derived from the name of the related table (users β†’ user_id) and the model's primary key (id).

Argument order

hasOne and hasMany do not expect their two keys in the same order. Refer to the signatures below rather than your intuition.

// hasOne(related, $foreign_key [related table], $local_key [this table])
$this->hasOne(Profile::class, 'user_id', 'id');

// hasMany(related, $local_key [this table], $foreign_key [related table])
$this->hasMany(Post::class, 'id', 'author_id');

// belongsTo(related, $foreign_key [this table], $local_key [related table])
$this->belongsTo(User::class, 'author_id', 'id');

// belongsToMany(related, $primary_key [this table], $foreign_key [related table])
$this->belongsToMany(Tag::class, 'id', 'tag_id');
Method call vs. property access difference
  • $post->author() β€” returns the relationship object (useful for adding constraints: $post->author()->where('active', true)->first()).
  • $post->author β€” returns the results (instance / Collection).

Lazy loading​

When you access a relationship as a property, Barry runs the query at that moment. The result is then kept in memory on the model instance: subsequent accesses return the same already-loaded object without re-running a query.

$post = Post::retrieve(42);

$post->author; // 1 query executed here
$post->author; // no query: same instance returned
N+1 problem

Lazy loading is convenient but becomes expensive in a loop: each model triggers its own query.

$posts = Post::all();        // 1 query

foreach ($posts as $post) {
echo $post->author->name; // 1 query PER post β†’ N+1 queries
}

For 100 posts, that makes 101 queries. The next section shows how to reduce this number to 2.

Eager loading​

The eager() method lets you preload one or more relationships in a single grouped query (WHERE ... IN (...)), thus avoiding the N+1 problem. The relationships are resolved right after the main query and pre-assigned to each parent model.

use App\Models\Post;

// 2 queries total, regardless of the number of posts:
// 1) SELECT * FROM posts
// 2) SELECT * FROM users WHERE id IN (...)
$posts = Post::eager('author')->get();

foreach ($posts as $post) {
echo $post->author->name; // no additional query
}

Load multiple relationships at once by passing an array:

$posts = Post::eager(['author', 'tags'])->get();

eager() works with all four relationship types (hasOne, hasMany, belongsTo, belongsToMany). Once preloaded, the relationship behaves exactly like property access, but without an additional query:

$masters = PetMaster::eager(['pets', 'firstPet'])->get();

foreach ($masters as $master) {
$master->pets; // already-loaded Collection
$master->firstPet; // already-loaded instance
}
Keep in mind
  • eager() chains onto the query and applies at the time of get().
  • If no related record exists, a "many" relationship returns an empty Collection and a "one" relationship returns null.
  • The list of relationships to preload is reset after each get(): it does not "leak" into the next query on the same model.

Configuration properties​

Barry offers several configuration properties to customize the model's behavior:

Timestamps​

By default, Barry automatically manages the created_at and updated_at columns. You can disable this behavior:

class Todo extends Model
{
/**
* Indicates whether the model should manage timestamps.
*
* @var bool
*/
protected bool $timestamps = false;
}

You can customize the names of the timestamp columns:

class Todo extends Model
{
protected string $created_at = 'date_creation';
protected string $updated_at = 'date_modification';
}

Hidden fields​

To exclude certain attributes during JSON serialization or conversion to an array:

class User extends Model
{
/**
* The attributes that should be hidden.
*
* @var array
*/
protected array $hidden = ['password', 'remember_token'];
}

Attribute casting​

To automatically convert attribute types on read ($model->attribute):

class Todo extends Model
{
/**
* The attribute type conversions.
*/
protected array $casts = [
'status' => 'int',
'budget' => 'float',
'rating' => 'double', // alias for float
'is_active' => 'bool', // 'boolean' works too
'due_date' => 'date', // -> Carbon
'meta' => 'array', // JSON -> associative array
'settings' => 'json', // JSON -> stdClass
];
}
CastEffect
intCast to int
float / doubleCast to float
bool / booleanCast to bool
dateCarbon\Carbon instance
arrayjson_decode to associative array
jsonjson_decode to stdClass

Columns treated as dates​

All columns listed in $dates (in addition to created_at, updated_at, expired_at, logged_at, and signed_at, which are recognized automatically) are wrapped in a Carbon\Carbon instance on read:

class Todo extends Model
{
protected array $dates = ['scheduled_for', 'completed_at'];
}

$todo = Todo::retrieve(1);
echo $todo->scheduled_for->diffForHumans(); // Carbon API available

Soft delete​

Bow provides a Bow\Database\Barry\Traits\SoftDelete trait that turns calls to delete() into an update of a deleted_at column instead of a physical DELETE.

1. Schema β€” add the deleted_at column to your table. The addSoftDelete() method is available in migrations:

$this->create('todos', function (Table $table) {
$table->addIncrement('id');
$table->addString('title');
$table->addTimestamps();
$table->addSoftDelete(); // adds a nullable `deleted_at` column
});

2. Model β€” add the trait:

use Bow\Database\Barry\Model;
use Bow\Database\Barry\Traits\SoftDelete;

class Todo extends Model
{
use SoftDelete;

// Optional: customize the column name
protected string $deleted_at = 'archived_on';
}

3. Usage

$todo = Todo::retrieve(1);

$todo->delete(); // UPDATE: deleted_at = NOW()
$todo->trashed(); // true
$todo->restore(); // UPDATE: deleted_at = NULL
$todo->forceDelete(); // physical DELETE

4. Queries

// Active rows only
Todo::withoutTrashed()->get();

// Archived rows only
Todo::onlyTrashed()->get();

// All rows (active + archived)
Todo::withTrashed()->get();
Explicit filtering

Global queries like Todo::all() or Todo::where(...)->get() return all rows, including soft-deleted ones. This is intentional: Bow does not apply an automatic global scope. Always use Todo::withoutTrashed() when you want to exclude archived rows.

5. Events

The model.deleting / model.deleted hooks continue to fire on delete() (a soft delete is still a delete from a business standpoint). Four additional events are available:

Todo::restoring(fn ($model) => /* before restore */);
Todo::restored(fn ($model) => /* after restore */);
Todo::forceDeleting(fn ($model) => /* before forceDelete */);
Todo::forceDeleted(fn ($model) => /* after forceDelete */);

Model events​

Barry lets you intercept various operations on models via events:

use App\Models\Todo;

// Before creation
Todo::creating(function ($model) {
// Executed before insertion
});

// After creation
Todo::created(function ($model) {
// Executed after insertion
});

// Before update
Todo::updating(function ($model) {
// Executed before the update
});

// After update
Todo::updated(function ($model) {
// Executed after the update
});

// Before deletion
Todo::deleting(function ($model) {
// Executed before deletion
});

// After deletion
Todo::deleted(function ($model) {
// Executed after deletion
});

Pagination​

To paginate the results of your queries:

use App\Models\Todo;

// Retrieves 15 items per page, page 1
$todos = Todo::paginate(15, 1);

// With the third parameter for the chunk
$todos = Todo::paginate(15, 1, 50);

Utility methods​

Touch​

To update only the updated_at timestamp:

$todo = Todo::retrieve(1);
$todo->touch();

Retrieve and delete​

To retrieve a record and delete it in a single operation:

$todo = Todo::retrieveAndDelete(1);

Attribute access​

Beyond the $model->attribute syntactic sugar, several methods let you manipulate the attribute array explicitly β€” useful for generic code:

$todo->getAttributes();              // all attributes (array)
$todo->getAttribute('title'); // a single attribute (null if absent)
$todo->setAttribute('title', '...'); // assignment
$todo->setAttributes([...]); // full replacement

Models implement ArrayAccess (via the ArrayAccessTrait trait) and JsonSerializable, which allows:

$todo['title'];                   // == $todo->title
isset($todo['title']); // existence
$todo['title'] = 'New'; // assignment
json_encode($todo); // uses jsonSerialize() (respects $hidden)

Primary key metadata​

$todo->getKey();      // column name (e.g. 'id')
$todo->getKeyType(); // configured type (e.g. 'int')
$todo->getKeyValue(); // current value (null on a non-persisted model)

Conversion to array or JSON​

$todo = Todo::retrieve(1);

// Conversion to array
$array = $todo->toArray();

// Conversion to JSON
$json = $todo->toJson();

Is something missing?

If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.