-
Notifications
You must be signed in to change notification settings - Fork 1
The Model class
Index > Database > The Model Class
The SnooPHP\Model\Model class provides an elegant interface to interact with the database models (i.e. database entities and inter-entities relationships).
It is intentionally largely inspired by Laravel's Eloquent ORM.
An entity is something that may exist independently (a user, a group). Entities are interconnected by relationships (for example a person may belong to one or many groups). In a relational database both are implemented as tables.
The Model class in SnooPHP exposes a set of methods to interact with a designated table of the database.
To create a new model, create a new PHP class that extends the Model class like this:
<?php
namespace App\Model;
use SnooPHP\Model\Model;
class User extends Model
{
// User class
}In general, we create as many models as tables in the database: each model has a 1:1 connection with a database table.
This connection is determined by the name of the associated table. By default, the connection is named after the name of the model class:
| Model class | table name |
|---|---|
User |
users |
Group |
groups |
Membership |
memberships |
However you may want to change the name of the connection. To do so, you must override the Model::tableName() static method:
class User extends Model
{
/**
* Set 'persons' as associated table
*/
public static function tableName()
{
return "persons";
}
}You may even pass the model fully-qualified class name to the Table constructor instead of the table name, so that if you change the model connection the migration will automatically apply this change and create the new table for you:
// Table will execute 'App\Model\User::tableName()' to retrieve the table name
$users = new Table("App\Model\User");
$users->id();Each model class is bound to a specific database connection ("master" by default). You can change the default database connection by setting the static property $dbName or with Model::setDbName(string $dbName = "master"):
User::setDbName("local");The Model class makes a strong assumption about the associated table: there exists a single, primary key, auto-increment, integer column named id. As you will see, it is convenient to define your entity models with such a column.
However the Model class can be used even without an id column, which is usually the case of relationships models:
// No id column but a composite primary key
$memberships = new Table("App\Model\Membership");
$memberships->uint("user_id")->primaryComposite();
$memberships->uint("group_id")->primaryComposite(true);The Table::id(string $name = "id", int $size = 16) method creates an id column named id for you. If you want to use a different name for the id column you can provide it as the first parameter. In that case you should also change the static property Model::$idColumn of the model class to reflect this change:
class User extends Model
{
// Name of the id column
protected static $idColumn = "num";
}The Model class exposes a set of static and non-static methods to retrieve entities from the database.
Results are returned as instances of the model class on which the static method was called (i.e. User::find(1) returns an instance of User). This means that we can define non-static methods on models and call them on retrieved objects:
$u = User::find(2);
$g = $u->groups(); // Get a list of groups this user belongs to
return Response::json($g);Table values can be accessed as class properties:
$user = User::find(3);
echo "user #{$user->id} is {$user->username} with email {$user->email}";Some select methods like Model::find() returns a single instance. Other methods, such as Model::select() returns a SnooPHP\Model\Collection object, which holds an array of instances that can be retrieved with Collection::array():
$users = User::select("where id > 50")->array();
var_dump($users); A static method that returns a single result from this query:
select $tableName.* from $tableName where $idColumn = $idif $idColumn == null the default id column is used.
$user = User::find("snoopy", "username"); // find user with username = snoopy
$user = User::find(1); // find user with id = 1This static method allows us to perform a more general query. It returns a Collection object or false if query execution fails. As with Db::query(), you can safely bind parameters using both binding styles. The query executed is:
select $tableName.* from $tableName $queryStringThis allows for more flexibility:
$groups = Group::select(); // All groups
$groups = Group::select("where created_at >= now() - interval 1 day"); // Recently created groups
$groups = Group::select(", memberships where memberships.group_id = groups.id and memberships.user_id = ?", [2]); // Groups user with id = 2 belongs toOne-to-one relationship. $forClass is the name of the "foreign model" class. $forColumn is the name of the foreign table column on which we join (by default "$className_id"):
select F.* from $tableName as R, $forTableName as F where R.$idColumn = F.$forColumn and R.$idColumn = :idReturns a single instance of the foreign model class:
$settings = User::find(1)->has("App\Model\Settings");Model::hasMany(string $forClass, string|null $forColumn = null, string $condition = "", array $conditionParams = [])
One-to-many relationship. $condition and $conditionParams allow to define an additional condition:
select F.* from $tableName as R, $forTableName as F where R.$idColumn = F.$forColumn and R.$idColumn = :id $conditionReturns a Collection object with instances of the foreign model class:
$posts = User::find(1)->hasMany("App\Model\Post", null, "where F.created_at > now() - interval 1 day");One-to-one and One-to-many inverse relationship:
select R.* from $refTableName as R, $tableName as F where R.$idColumn = F.$forColumn and F.$forColumn = :idReturns a single instance of the referenced model class:
$user = Post::find(1)->belongsTo("App\Model\User");The Model class provide a single function to insert and update table rows, the Model::save(bool $create = false) method. It works like a "create or update" statement: it tries to insert the row; if a duplicate primary key is found it tries to update it. If $create === true it never tries to update the row, but rather throws a PDOException.
The data to insert is retrieved from the model properties matching the column names:
// Insert a new user
$user = new User();
$user->username = "linus";
$user->password = "qwerty";
$user->email = "[email protected]";
var_dump($user->save());The returned value is the model itself or false if an error occured.
The returned model is fetched from the database after the insert/update, thus it contains any value automatically generated by the database (id column, timestamps, default values for columns whose value was not specified, etc.).
The Model class also has a constructor that allows you to quickly specify model data using an associative array:
$user = new User([
"username" => "charlie",
"email" => "[email protected]",
"password" => "qwerty"
});
$user = $user->save();
echo "id of {$user->username} is {$user->id}";Model::delete() is used to delete a single model. If $idColumn is set then it is used in the where clause, otherwise all available column values are used:
User::find("bruce wayne", "username")->delete();To delete multiple models we can use Model::deleteWhere(string $condition = "", array $conditionParams = []) static method, which allows us to specify a where condition:
Token::deleteWhere("expires_at < now()"); // Delete expired tokensFinally, Model::purge() static method execute a truncate statement:
truncate $tableName;Note: MySQL doesn't allow to truncate a table referenced by other tables
If you simply want to reset the auto increment value, you can also use Model::resetAutoIncrement() static method.
The Model class has a few options that you can set by changing the corresponding static property:
- the name of the id column
Model::$idColumn. If you're table doesn't have an id column you don't have to worry about this; - the database connection to use can be set with the static method
Model::setDbName(string $dbName = "master")or by changing the static propertyModel::$dbName; - you can tell it to automatically cast values in/out of the database in the associative array
Model::$casts:protected static $casts = [ "user_id" => "int", "group_id" => "int", "is_admin" => "bool" ]
- the
Model::$sutosarray contains a set of columns that are automatically updated by the database and should not be sent in update queries (for example timestamp columns with theon updateproperty); - values can be automatically converted to/from json format. Add the name of the json column in the
Model::$jsonsarray
Methods that return many results return them inside a Collection object.
The Collection class is a container for an array of models (ideally spawned from the same model class) that exposes a set of methods to operate with the models.
The most basic ones are:
-
Collection::array()returns the array of models; -
Collection::size()andCollection::num()return the number of models contained; -
Collection::empty()returnstrueif the number of models is zero; -
Collection::first(),Collection::last()andCollection::get(int $i = 0)respectively return the first, the last and the$i-th model.
To find a particular model inside the collection we can use Collection::find(Model|Callable $criteria) where $criteria can either be a model (in which case the corresponding index is returned) or a callable (in which case the returned value is the model found). If no match is found null is returned:
// Find first user that has no group
$match = $users->find(function($u) {
return empty($u->groups());
});find() returns at most one result. Collection::each(Callable $iterator) is a more flexible method that simply iterator $iterator function over all models. Models are passed by reference, thus you can use each() to alter the models in the collection:
$users->each(function($u) {
// Remove the password before sending users
unset($u->password);
});
return Response::json($users);The returned value is the collection itself.
Finally you can use Collection::append(Collection $collection) that appends the models of $collection to the current collection and returns it.
Note that you are allowed to have a mix of different models inside a collection
A simplistic social network setup:
use SnooPHP\Model\Db;
use SnooPHP\Model\Model;
use SnooPHP\Model\Table;
$users = new Table("User");
$users->id();
$users->string("username")->notNullable()->unique();
$users->string("email")->notNullable()->unique();
$users->string("password")->notNullable();
$users->timestamp("created_at");
$posts = new Table("Post");
$posts->id("id", 32);
$posts->uint("user_id")->references("users", "id", "cascade");
$posts->text("text")->notNullable();
$posts->timestamp("created_at");
$reactions = new Table("Reaction");
$reactions->uint("post_id")->references("posts", "id", "cascade")->primaryComposite();
$reactions->uint("user_id")->references("users", "id", "cascade")->primaryComposite(true);
$reactions->string("type")->notNullable()->default("'like'"); // Enum not still implemented
$reactions->timestamp("created_at");
// Run migration
$migration = new Migration("master", [
$users,
$posts,
$reactions
]);
$migration->run();
class User extends Model
{
/**
* Get user posts
*
* @return Collection
*/
public function posts()
{
return $this->hasMany("Post");
}
/**
* Return posts the user reacted to
*
* @return Collection
*/
public function reactedPosts()
{
$pt = Post::tableName();
$rt = Reaction::tableName();
return Post::select(", $rt where $rt.post_id = $pt.id and $rt.user_id = ?", [$this->id]);
}
/**
* Find with username or email
*
* @param string $login username or email
*
* @return User
*/
public static function findByLogin($login)
{
return static::select("where username = :login or email = :login", ["login" => $login])->first();
}
}
class Post extends Model
{
/**
* Return user that created this post
*
* @return User
*/
public function user()
{
return $this->belongsTo("User");
}
/**
* Return user that reacted to this post
*
* @return Collection
*/
public function supporters()
{
$ut = User::tableName();
$rt = Reaction::tableName();
return User::select(", $rt where $rt.user_id = $ut.id and $rt.post_id = ?", [$this->id]);
}
}
class Reaction extends Model
{
// Relationship model
}
if ($argc > 1)
{
$user = find($argv[1], "username");
if ($user)
{
$user->posts = $user->posts();
$user->reactedPosts = $user->reactedPosts();
echo to_json($user);
}
}