Barry ORM
Barry is the ORM (Object Relation Mapping) built into BowPHP.
Introductionβ
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.
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
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
{
//
}
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β
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
}
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();
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).
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');
$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
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
}
eager()chains onto the query and is applied at the time ofget().- If no related record exists, a "many" relationship returns an empty
Collectionand a "one" relationship returnsnull. - 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
];
}
| Cast | Effect |
|---|---|
int | Cast to int |
float / double | Cast to float |
bool / boolean | Cast to bool |
date | Carbon\Carbon instance |
array | json_decode to associative array |
json | json_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();
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.