Skip to main content
Version: CANARY 🚧

Barry ORM

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

Introduction​

info

An ORM (Object Relation Mapping) is a way of relating tables to one another 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 for data in your tables, as well as insert new records.

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

Prerequisites

Before getting started, make sure you configure 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 of 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​

Notice 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 to "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 indicate the type of the primary key and disable auto-increment (for example for UUIDs or string-based composite 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 the fly:

$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 also let you retrieve data:

// 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 (defaults to created_at), 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​

The Barry all method will return all results in the model's table. Since each Barry model serves as a query builder, you can also add constraints to queries and 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();

Selecting columns​

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 insertion
Todo::insert([
'title' => 'New task',
'status' => 'pending'
]);

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

Checking existence​

use App\Models\Todo;

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

// Check by column and value
$exists = Todo::exists('email', 'user@example.com');

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 presents the most common Query Builder methods. For complete documentation with all advanced options, see the Query Builder page.

Retrieving aggregates​

You can also use the count, sum, max, and other aggregate 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 define the 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 and value pairs representing the columns to be updated.

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 through 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 relation object. Accessing this method as a property triggers the 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

With no keys given, Barry looks for profiles.user_id β€” the foreign key is named after the declaring model (User β†’ user_id), and it lives on the related (profiles) table.

hasMany β€” One to many​

A user has many 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

Here too the default foreign key is posts.user_id, derived from the declaring User model β€” not post_id. Pass the key explicitly if your column differs (see Customizing keys).

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 many tags through 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 keys​

All methods accept two optional parameters to specify the join columns. For hasOne and hasMany the foreign key is inferred from the declaring model's table (User β†’ user_id); for belongsTo it is inferred from the related model. The local key defaults to the model's primary key (id).

Argument order

hasOne and hasMany take their keys in the same order β€” (related, foreign_key, local_key) β€” where the foreign key is the column on the related table and the local key the referenced column on this one.

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

// hasMany(related, $foreign_key [related table], $local_key [this table])
$this->hasMany(Post::class, 'author_id', '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 relation 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 executes 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 running the query again.

$post = Post::retrieve(42);

$post->author; // 1 query executed here
$post->author; // no query: the same instance is 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 as in 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
}
Key takeaways
  • eager() chains onto the query and is applied 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" onto the next query of 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 array conversion:

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 casts.
*/
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 records.

5. Events

The model.deleting / model.deleted hooks continue to fire on delete() (soft delete remains a deletion 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 through 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 the deletion
});

// After deletion
Todo::deleted(function ($model) {
// Executed after the 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);

Accessing attributes​

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.