Skip to content

The Model class

Andrea Mecchia edited this page Jul 4, 2018 · 5 revisions

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.

Models and tables

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 id column

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";
}

Select methods

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); 

Model::find(mixed $id, string|null $idColumn = null)

A static method that returns a single result from this query:

select $tableName.* from $tableName where $idColumn = $id

if $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 = 1

Model::select(string $queryString = "", array $queryParams = [])

This 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 $queryString

This 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 to

Model::has(string $forClass, string|null $forColumn = null)

One-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 = :id

Returns 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 $condition

Returns 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");

Model::belongsTo(string $refClass, string|null $forColumn = null)

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 = :id

Returns a single instance of the referenced model class:

$user = Post::find(1)->belongsTo("App\Model\User");

Insert and udpate methods

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}";

Delete methods

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 tokens

Finally, 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.

Model options

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 property Model::$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::$sutos array 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 the on update property);
  • values can be automatically converted to/from json format. Add the name of the json column in the Model::$jsons array

Collections

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() and Collection::num() return the number of models contained;
  • Collection::empty() returns true if the number of models is zero;
  • Collection::first(), Collection::last() and Collection::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 use case example

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);
	}
	
}

The Node class >

Clone this wiki locally