Appearance
Ashes ORM
Table of Contents
- Introduction
- Generating Model Classes
- Model Conventions
- Retrieving Models
- Fluent Connection
- Eager Loading
- Saving Models
- Updating Models
- Deleting Models
- Model State and Serialization
- Transactions
Introduction
Phenix uses the Data Mapper pattern for the modeling layer, allowing a clear separation between business logic and data access logic. Models use class properties with PHP attributes to map table columns in the database to model properties. Attributes also allow you to define relationships.
Generating Model Classes
Phenix provides a command to generate models with additional options to create a controller, migration, model query builder, and model collection. To create a model, you can use the following command:
sh
php phenix make:model UserCommand Options
Create a model with a controller:
sh
php phenix make:model User --controller
php phenix make:model User -cCreate a model with a migration:
sh
php phenix make:model User --migration
php phenix make:model User -mCreate with a custom collection:
sh
php phenix make:model User --collection
php phenix make:model User -cnCreate with a custom query builder:
sh
php phenix make:model User --query
php phenix make:model User -qbCreate with a controller, migration, query builder, and model collection:
sh
php phenix make:model User --all
php phenix make:model User -aDatabase models extend from Phenix\Database\Models\DatabaseModel:
php
<?php
declare(strict_types=1);
namespace App\Models;
use Phenix\Database\Models\DatabaseModel;
class User extends DatabaseModel
{
public static function table(): string
{
//
}
}Model Conventions
Defining Table
When a model is created, the first thing to do is to define the database table.
php
public static function table(): string
{
return 'users';
}Model Properties
Model properties are defined using PHP attributes. Here are the available attributes:
Available Attributes
Id: Defines a property as the primary key of the model.
php
use Phenix\Database\Models\Attributes\Id;
#[Id]
public int $id;Column: Defines a property as a column in the database.
php
use Phenix\Database\Models\Attributes\Column;
#[Column]
public string $name;DateTime: Defines a property as a date/time column.
php
use Phenix\Database\Models\Attributes\DateTime;
#[DateTime(name: 'created_at', autoInit: true)]
public Date $createdAt;ForeignKey: Defines a property as a foreign key.
php
use Phenix\Database\Models\Attributes\ForeignKey;
#[ForeignKey(name: 'user_id')]
public int $userId;Hidden: Marks a property to be excluded from serialized output (toArray() and toJson()). This attribute extends Column, so the property remains part of model persistence.
php
use Phenix\Database\Models\Attributes\Hidden;
#[Hidden]
public string $password;Relationships
Relationships between models are defined using specific attributes:
BelongsTo: Defines a one-to-many relationship where the model belongs to another model.
php
use Phenix\Database\Models\Attributes\BelongsTo;
use Phenix\Database\Models\Attributes\ForeignKey;
#[ForeignKey(name: 'user_id')]
public int $userId;
#[BelongsTo(foreignProperty: 'userId')]
public User $user;The name of the model property that was assigned as the foreign key is passed as a parameter to the BelongsTo attribute. In turn, the foreign key maps to the foreign key column name in the database to the ForeignKey attribute.
HasMany: Defines a one-to-many relationship where the model has many of another model.
php
use Phenix\Database\Models\Attributes\HasMany;
#[HasMany(model: Product::class, foreignKey: 'user_id')]
public Collection $products;BelongsToMany: Defines a many-to-many relationship.
php
use Phenix\Database\Models\Attributes\BelongsToMany;
#[BelongsToMany(
table: 'invoice_product',
foreignKey: 'product_id',
relatedModel: Invoice::class,
relatedForeignKey: 'invoice_id'
)]
public Collection $invoices;Property-Column Mapping
When the column name in the database and the property name in the model are different, you can assign a column name to be used in the mapping:
php
use Phenix\Database\Models\Attributes\Id;
use Phenix\Database\Models\Attributes\Column;
#[Id(name: 'idKey')] // Column name in DB
public int $id;
#[Column(name: 'customerName')] // Column name in DB
public string $name;This is what a model looks like with its properties defined:
php
<?php
declare(strict_types=1);
namespace App\Models;
use Phenix\Database\Models\Attributes\Column;
use Phenix\Database\Models\Attributes\DateTime;
use Phenix\Database\Models\Attributes\HasMany;
use Phenix\Database\Models\Attributes\Id;
use Phenix\Database\Models\Collection;
use Phenix\Database\Models\DatabaseModel;
use Phenix\Util\Date;
class User extends DatabaseModel
{
#[Id]
public int $id;
#[Column]
public string $name;
#[Column]
public string $email;
#[DateTime(name: 'created_at', autoInit: true)]
public Date $createdAt;
#[DateTime(name: 'updated_at')]
public Date|null $updatedAt = null;
#[HasMany(model: Product::class, foreignKey: 'user_id')]
public Collection $products;
#[HasMany(model: Comment::class, foreignKey: 'user_id', chaperone: true)]
public Collection $comments;
public static function table(): string
{
return 'users';
}
}Retrieving Models
The model query builder allows executing queries and mapping the results into models. If the query retrieves multiple records, they are added to a collection.
Single Model
To query a single model, you can use the first method on the query builder. This method retrieves the first record that matches the query criteria and maps it to a model instance. For example, to get a user with a specific ID, you can use the whereEqual method to specify the condition and then call first to get the user model. More information can be found in the query builder section.
php
$user = User::query()->whereEqual('id', 1)->first();
echo $user->name;Phenix ORM also provides a shorthand syntax for querying models. Instead of using the query builder, you can use the find method directly on the model class to retrieve a single record by its primary key. This method accepts the primary key value and an optional array of columns to select:
php
$user = User::find(1, ['id', 'name']);
echo $user->name;This approach simplifies the code and makes it more readable when you need to fetch a single model by its primary key.
Collections
When a query retrieves multiple records, they are automatically mapped into a collection, which provides an array-like interface with additional methods for filtering, sorting, and manipulating the data. Collections make it easy to work with groups of models, allowing you to iterate over them, apply transformations, and perform bulk operations efficiently.
php
$users = User::query()->selectAllColumns()->get();
foreach ($users as $user) {
echo $user->name;
}Pagination
The paginate method is used to retrieve a paginated set of results from the database. It simplifies the process of fetching and displaying paginated data, making it easier to handle large datasets by breaking them into manageable chunks.
php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\User;
use Phenix\Http\Request;
use Phenix\Http\Response;
use Phenix\Http\Controller;
class UserController extends Controller
{
public function index(Request $request): Response
{
$users = User::query()
->selectAllColumns()
->paginate($request->getUri());
return response()->json($users);
}
}Paginate Signature
php
use League\Uri\Http;
use Phenix\Database\Paginator;
public function paginate(Http $uri, int $defaultPage = 1, int $defaultPerPage = 15): PaginatorHttp $uri: An instance of theHttpclass from theLeague\Uripackage, representing the current URI. This is used to extract query parameters for pagination.int $defaultPage: The default page number to use if no page parameter is present in the URI. Defaults to1.int $defaultPerPage: The default number of items per page if no per_page parameter is present in the URI. Defaults to15.
Incremental Column Selection
Use addSelect(...) to append columns to an existing select clause:
php
$users = User::query()
->select(['id'])
->addSelect(['name', 'email'])
->get();Fluent Connection
Use on(...) when you want to run ORM operations on a specific connection:
php
$users = User::on('sqlite')->get();
$user = User::on('sqlite')->find(1);This fluent API also supports write operations:
php
$user = User::on('sqlite')->create([
'name' => 'Fluent User',
'email' => 'fluent@example.com',
]);Eager Loading
Use with(...) to load relationships together with the main model query:
php
$post = Post::query()
->selectAllColumns()
->with('user')
->first();You can use shorthand syntax to select columns from the relationship:
php
$post = Post::query()
->with('user:id,name')
->first();Constrained Eager Loading
You can constrain relationship queries using closures:
php
use Phenix\Database\Models\Relationships\HasMany;
$user = User::query()
->with([
'products' => function (HasMany $relation): void {
$relation->query()->select(['id', 'description', 'user_id']);
},
])
->first();Pivot Columns in BelongsToMany
For many-to-many relationships, you can include pivot fields:
php
use Phenix\Database\Models\Relationships\BelongsToMany;
$invoices = Invoice::query()
->with([
'products' => function (BelongsToMany $relation): void {
$relation->withPivot(['quantity', 'value']);
},
])
->get();Chaperone in HasMany
When needed, you can assign parent models to children using chaperone:
php
use Phenix\Database\Models\Relationships\HasMany;
$user = User::query()
->with([
'products' => function (HasMany $relation): void {
$relation->withChaperone();
},
])
->first();Nested Eager Loading
Nested relationships are supported using dot notation:
php
$comment = Comment::query()
->with([
'product:id,description,user_id,created_at',
'product.user:id,name,email,created_at',
])
->first();Saving Models
The save method is used to persist a model instance to the database. This method handles both the creation of new records and the updating of existing records.
php
$user = new User();
$user->name = 'John Doe';
$user->email = 'john@example.com';
$user->save();The create method is a static method used to create a new model instance with the given attributes. This method simplifies the process of instantiating a model, setting its properties, and saving it to the database in a single step.
php
$user = User::create([
'name' => 'John Doe',
'email' => 'john@example.com',
'created_at' => Date::now(),
]);Updating Models
To update an existing model, first retrieve the model, make the necessary changes, and then call the save method:
php
$user = User::find(1);
$user->email = 'newemail@example.com';
$user->save();Deleting Models
To delete a model from the database, call the delete method:
php
$user = User::find(1);
$user->delete();Model State and Serialization
Useful model methods:
isExisting(): indicates whether the model is already persisted.toArray(): returns model data as an associative array.toJson(): returns model data as JSON.
php
$user = User::find(1);
if ($user?->isExisting()) {
$payload = $user->toArray();
$json = $user->toJson();
}Hidden Attributes in Serialization
Use #[Hidden] to omit sensitive properties from serialized output. Internally, DatabaseModel::toArray() skips properties marked with Hidden, and toJson() inherits that behavior because it serializes toArray().
php
use Phenix\Database\Models\Attributes\Column;
use Phenix\Database\Models\Attributes\Hidden;
use Phenix\Database\Models\Attributes\Id;
use Phenix\Database\Models\DatabaseModel;
class User extends DatabaseModel
{
#[Id]
public int $id;
#[Column]
public string $name;
#[Hidden]
public string $password;
public static function table(): string
{
return 'users';
}
}
$user = new User();
$user->id = 1;
$user->name = 'John Hidden';
$user->password = 'secret';
$array = $user->toArray();
$json = $user->toJson();Expected array output:
php
[
'id' => 1,
'name' => 'John Hidden',
]$json also excludes the password field.
Transactions
ORM operations support the same transaction system as the query builder.
Transaction Callback
Use DB::transaction(...) for automatic commit/rollback:
php
use Phenix\Database\TransactionManager;
use Phenix\Facades\DB;
DB::transaction(function (TransactionManager $tx): void {
User::create([
'name' => 'Alice',
'email' => 'alice@example.com',
], $tx);
$user = User::find(1, ['*'], $tx);
$user->name = 'Alice Updated';
$user->save($tx);
});If the callback throws an exception, changes are rolled back.
Manual Transactions
Use DB::beginTransaction() for explicit control:
php
use Phenix\Facades\DB;
$tx = DB::beginTransaction();
try {
User::create([
'name' => 'Bob',
'email' => 'bob@example.com',
], $tx);
$tx->commit();
} catch (Throwable $e) {
$tx->rollBack();
throw $e;
}Using withTransaction
withTransaction(...) binds a model query builder to an active transaction:
php
use Phenix\Database\TransactionManager;
use Phenix\Facades\DB;
DB::transaction(function (TransactionManager $tx): void {
$user = User::query()
->withTransaction($tx)
->find(1);
if ($user) {
$user->name = 'Updated in transaction';
$user->save($tx);
}
});Model Methods with TransactionManager
The following ORM methods accept an optional TransactionManager:
User::query(TransactionManager|null $tx = null)User::create(array $attributes, TransactionManager|null $tx = null)User::find(string|int $id, array $columns = ['*'], TransactionManager|null $tx = null)$model->save(TransactionManager|null $tx = null)$model->delete(TransactionManager|null $tx = null)
You can also omit the manager inside a transaction callback; context propagation keeps operations in the active transaction.