Lifecycle Callbacks
Lifecycle callbacks are methods on a model that get called automatically when certain actions are performed on that model. For instance, if you create a new User and ask a Repository to save it, the model's "beforeCreate()" method will get called before the User gets saved to the database, and the "afterCreate()" method will get called after a record for that User gets inserted.
There are various uses where these Lifecycle Callbacks could come in handy, and I'll try to give an example or two.
List of Callbacks
Callback | Description |
---|---|
afterCreate() | Called after insertion happens when a Repository is asked to save a NEW object. |
afterGet($property, $value) | Called after a model property gets grabbed. |
afterSave() | Called after an update happens when a Repository is asked to save an EXISTING object. Also called after an insertion happens when a repo is asked to save a NEW object (called right AFTER beforeCreate). |
afterSet($property, $value) | Called after a model property gets set. |
beforeCreate() | Called before insertion happens when a Repository is asked to save a NEW object. |
beforeGet($property) | Called before a model property gets grabbed. |
beforeSave() | Called before an update happens when a Repository is asked to save an EXISTING object. Also called before an insertion happens when a repo is asked to save a NEW object (called right AFTER afterCreate). |
beforeSet($property, $value) | Called before a model property gets set. I.E. If you set a name on a User object like so: $user->name = 'Bob', the beforeSet method will get called with "name" as the property and 'Bob' as the value before the setting takes place. |
delete() | Called before deletion happens when a Repository is asked to delete an object. |
onLoad() | Models are populated/hydrated AFTER getting created. So in the constructor you can't create computed properties. This method is called after the model has been populated so you can compute new properties as necessary. I.E. Concatenate a first name and last into a "name" property. |
Usage Examples
If you haven't used callbacks before, it may not be immediately clear how or why you might want to use them. The following are a few examples of common use cases where callbacks can make your life easier.
Setting a Computed Default Value
Alright, say you have a User model and you want to have a "joinedDate" property that represents when that user first joined your site/app. The easiest way to do this would be to use the beforeCreate() callback. This means adding a beforeCreate() method to your User model that maybe looks something like this:
class User extends \Cora\Model {
public $model_attributes = [
'id' => [
'type' => 'int',
'primaryKey' => true
],
'name' => [
'type' => 'varchar'
],
'email' => [
'type' => 'varchar',
'index' => true
],
'joinedDate' => [
'type' => 'date'
]
];
public function beforeCreate()
{
$this->joinedDate = date('Y-m-d');
}
}
Since beforeCreate() gets called BEFORE the saving of the object to the database takes place, that means the 'joinedDate' property will have the current date and get saved to the database as such.
Doing Cleanup Before Deleting
Another common use case might be doing some sort of database cleanup before deleting an object. Say you have some sort of "Comment" object representing comments users can leave on content on your site. Let's also say for this example that Users can upload files as an attachment to a comment and those files are stored in a separate table/collection with a reference back to the parent. When you tell ADM to delete the parent comment like so:
$repo = \Cora\RepositoryFactory::make('Comment');
$comment = $repo->find($id);
$comment->delete();
Any related children WILL NOT get automatically deleted along with it (because ADM can't safely assume that deleting all related objects is desired behavior)! In order to avoid leaving unwanted orphans in the database, you can utilize the delete() callback to manually delete or reassign any related data.
class Comment extends \Cora\Model {
public $model_attributes = [
'id' => [
'type' => 'int',
'primaryKey' => true
],
'user' => [
'model' => 'User'
],
'text' => [
'type' => 'text'
],
'files' => [
'models' => 'FileAttachment'
]
];
public function delete()
{
// Code to handle files related to this comment...
}
}
Convert a Date String from mm-dd-yyyy to yyyy-mm-dd when Saving
Another area where callbacks could be useful is if you have a date in your model that is stored as a string in the "mm-dd-yyyy" format and you need to convert it to a "yyyy-mm-dd" string for insertion into a database.
And before I give this example, let me just point out that ADM is meant to work well with php DateTime objects. If you grab a model using a Repository that has a "date" or "datetime" database field, that field will automatically get converted into a DateTime object for you when it's populated into your model. Similarly, when you go to save your model, the DateTime will be grabbed as a "yyyy-mm-dd" string for use with the database automatically - no work needs to be done on your part as a developer.
However, if for whatever reason your model has a date that is being stored as a plain string, and you don't want to convert it into a DateTime object (see next example), then the following example could be useful to you:
class User extends \Cora\Model {
public $model_attributes = [
'id' => [
'type' => 'int',
'primaryKey' => true
],
'name' => [
'type' => 'varchar'
],
'email' => [
'type' => 'varchar',
'index' => true
],
'joinedDate' => [
'type' => 'varchar'
]
];
public function beforeSave()
{
// Save the real value of joinedDate to a temp variable
$this->joinedDateTemp = $this->joinedDate;
// Swap joinedDate from mm-dd-yyyy to yyyy-mm-dd
$timestamp = strtotime($this->joinedDate);
$this->joinedDate = date('Y-m-d', $timestamp);
}
public function afterSave()
{
// Swap joinedDate back to its mm-dd-yyyy value.
$this->joinedDate = $this->joinedDateTemp;
// You can optionally clear the temp variable we used.
$this->joinedDateTemp = null;
}
}
The above code makes "joinedDate" a string of yyyy-mm-dd format right before the User object is saved, and then swaps it back to its original mm-dd-yyyy format after the save is finished.
Grab Date in Particular Format
When ADM fetches date fields from a database, it converts them into php DateTime objects. If you've used DateTime object before, then you probably know you can get their value formatting in a certain way by doing:
echo $someDate->format('m-d-Y');
However, if for some reason you didn't want to have to specify the format anytime you display a date, you could utilize the beforeGet() method to always have it return in a particular way:
class User extends \Cora\Model {
public $model_attributes = [
'id' => [
'type' => 'int',
'primaryKey' => true
],
'name' => [
'type' => 'varchar'
],
'email' => [
'type' => 'varchar',
'index' => true
],
'joinedDate' => [
'type' => 'date'
]
];
public function beforeGet($prop)
{
if ($prop == 'joinedDate') {
// Save the real value of joinedDate to a temp variable
$this->joinedDateTemp = $this->joinedDate;
// Change the value of joinedDate to a formatted string
$this->joinedDate = $this->joinedDate->format('m-d-Y');
}
}
public function afterGet($prop, $value)
{
if ($prop == 'joinedDate') {
// Restore joined date.
$this->joinedDate = $this->joinedDateTemp;
}
}
}
Enforcing Restrictions on a Data Member
Another possible use for callbacks could be enforcing some set of restrictions on a particular data member within a model. For instance, let's say you have an "age" property on your User model and you want to ensure that only numbers get assigned to it. You can do this by utilizing the beforeSet() callback:
class User extends \Cora\Model {
public $model_attributes = [
'id' => [
'type' => 'int',
'primaryKey' => true
],
'name' => [
'type' => 'varchar'
],
'email' => [
'type' => 'varchar',
'index' => true
],
'age' => [
'type' => 'int'
]
];
public function beforeSet($prop, $value)
{
if ($prop == 'age') {
if (is_numeric($value) == false) {
// Throw some exception or something...
}
}
}
}
beforeGet() Gotchas
All data members and attributes on a model are intended to be public. Models really shouldn't be hiding domain logic and shouldn't need private or protected data members. They are akin to fancy structs for those familar with the term.
A potential gotcha is if you define a public data member on a model (since logically everything should be public) and then get confused as to why it's not triggering the callback if you access it. Public data members won't trigger the magic get() method that contains the callback logic. Only attempts to access normally inaccessible data will trigger get(). For this reason, you should define any computed or additional fields on a model that aren't part of the Attributes definition as protected and let the model make it accessible. Note: Any dynamically defined data members will also be public by default, and cause the same issue.