Skip to content

Commit

Permalink
Merge pull request #238 from atk4/feature/add-hook-doc-and-minor-fix
Browse files Browse the repository at this point in the history
Feature/add hook doc and minor fix
  • Loading branch information
romaninsh committed Sep 3, 2017
2 parents d474cc1 + 4c75fdf commit bd65c96
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 44 deletions.
2 changes: 2 additions & 0 deletions docs/advanced.rst
Expand Up @@ -86,6 +86,8 @@ new record is inserted, so those fields will be left as "null" after initial ins

If you wish, you can modify the code and insert historical records into other table.

.. _soft_delete:

Soft Delete
===========

Expand Down
57 changes: 42 additions & 15 deletions docs/fields.rst
Expand Up @@ -17,12 +17,12 @@ mapping into persistence-logic.

.. php:class:: Field
.. php:property:: default
.. php:attr:: default
When no value is specified for a field, default value is used
when inserting.

.. php:property:: type
.. php:attr:: type
Valid types are: string, integer, boolean, datetime, date, time.

Expand All @@ -37,33 +37,60 @@ Those are responsible for converting PHP native types to persistence
specific formats as defined in fields. Those methods will also change
name of the field if needed (see Field::actual)

.. php:property:: enum
.. php:attr:: enum
Specifies array containing all the possible options for the value.
You can set only to one of the values (loosely typed comparison
is used)

.. php:property:: mandatory
.. php:attr:: mandatory
Set this to true if field property is mandatory and must be non-null in order
for model to properly exist. Note that you can still set the NULL value to the
Set this to true if field value must not be NULL. You can set the NULL value to the
field, but you won't be able to save it.

.. php:property:: read_only
Example::

$model['age'] = 0;
$model->save();

$model['age'] = null;
$model->save(); // exception


.. php:attr:: required
Set this to true for field that may not contain "empty" value. You can't use NULL
or any value that is considered ``empty()`` by PHP. Some examples that are not
allowed are:

- empty string ""
- 0 numerical value or 0.00
- boolean false

Example::

$model['age'] = 0;
$model->save(); // exception

$model['age'] = null;
$model->save(); // exception


.. php:attr:: read_only
Modifying field that is read-only through set() methods (or array access) will
result in exception. :php:class:`Field_SQL_Expression` is read-only by default.

.. php:property:: actual
.. php:attr:: actual
Specify name of the Table Row Field under which field will be persisted.

.. php:property:: join
.. php:attr:: join
This property will point to :php:class:`Join` object if field is associated
with a joined table row.

.. php:property:: system
.. php:attr:: system
System flag is intended for fields that are important to have inside hooks
or some core logic of a model. System fields will always be appended to
Expand All @@ -72,25 +99,25 @@ or grids (see :php:meth:`Model::isVisible`, :php:meth:`Model::isEditable`).

Adding condition on a field will also make it system.

.. php:property:: never_persist
.. php:attr:: never_persist
Field will never be loaded or saved into persistence. You can use this flag
for fields that physically are not located in the database, yet you want
to see this field in beforeSave hooks.

.. php:property:: never_save
.. php:attr:: never_save
This field will be loaded normally, but will not be saved in a database.
Unlike "read_only" which has a similar effect, you can still change the
value of this field. It will simply be ignored on save. You can create
some logic in beforeSave hook to read this value.

.. php:property:: ui
.. php:attr:: ui
This field contains certain arguments that may be needed by the UI layer
to know if user should be allowed to edit this field.

.. php:property:: loadCallback
.. php:attr:: loadCallback
Specify a callback that will be executed when the field is loaded and
it is necessary to decode or do something else with loaded the value.
Expand Down Expand Up @@ -118,7 +145,7 @@ Note that if you use a call-back this will by-pass normal field typecasting.

See :ref:`Advanced::EncryptedField` for full example.

.. php:property:: saveCallback
.. php:attr:: saveCallback
Same as loadCallback property but will be executed when saving data. Arguments
are still the same::
Expand Down
194 changes: 168 additions & 26 deletions docs/hooks.rst
Expand Up @@ -4,44 +4,186 @@
Hooks
=====

.. php:class:: Model
Hook is a mechanism for adding callbacks. The core features of Hook sub-system
(explained in detail here http://agile-core.readthedocs.io/en/develop/hook.html)
include:

.. important::
- ability to define "spots" in PHP code, such as "beforeLoad".
- ability to add callbacks to be executed when PHP goes over the spot.
- prioritization of callbacks
- ability to pass arguments to callbacks
- ability to collect response from callbacks
- ability to break hooks (will stop any other hook execution)

Please never use ``$this`` inside your hook to refer to the model. The model
is always passed as a first argument. If you ever use ``$this`` then your
model will perform very weirdly when cloned::
:php:ref:`Model` implements hook trait and defines various hooks which will allow
you to execute code before or after various operations, such as save, load etc.

Model Operation Hooks
=====================

All of model operations (adding, updating, loading and deleting) have
two hooks - one that executes before operation and another that executes
after.

Those hooks are database-agnostic, so regardless where you save your
model data, your `beforeSave` hook will be triggered.

If database has transaction support, then hooks will be executed while
inside the same transaction:

- begin transaction
- beforeSave
- actual save
- reload (see :php:attr:`Model::_reload_after_save`)
- afterSave
- commit

if your afterSave hook creates exception, then the entire opperation will be rolled back.

Example with beforeSave
-----------------------

The next code snippet demonstrates a basic usage of a `beforeSave` hook. This one
will update field values just before record is saved::

$m->addHook('beforeSave', function($m) {
$this['name'] = 'John'; // WRONG!
$m['surname'] = 'Smith'; // GOOD
$m['name'] = strtoupper($m['name']);
$m['surname'] = strtoupper($m['surname']);
});

$m->insert([]);
// Will save into DB: ['surname'=>'Smith'];
$m->insert(['name'=>'John', 'surname'=>'Smith']);

// Will save into DB: ['name'=>'JOHN', 'surname'=>'SMITH'];

Arguments
---------

When you define a callback, then you'll receive reference to model from all the hooks.
It's important that you use this argument instead of $this to perform operation, otherwise
you can run into problems with cloned models.

Callbacks does non expect anything to be returned, but you can modify fields of the
model.

echo $m['name']; // Will contain 'John', which it shouldn't
// because insert() is not supposed to affect active record
Interrupting
------------

afterLoad hook
--------------
You can return false from afterLoad hook to prevent yielding of particular data rows.
You can also break all "before" hooks which will result in cancellation of the original action::

Use it like this::
$m->breakHook(false);

$model->addHook('afterLoad', function ($m) {
if ($m['date'] < $m->date_from) {
$m->breakHook(false); // will not yield such data row
}
// otherwise yields data row
});
If you break beforeSave, then the save operation will not take place, although model will assume
the operation was successful.

Also this approach can be used to prevent data row to be loaded. If you return false
You can also berak beforeLoad hook which can be used to skip rows::

$model->addHook('afterLoad', function ($m) {
if ($m['date'] < $m->date_from) {
$m->breakHook(false); // will not yield such data row
}
// otherwise yields data row
});

This will also prevent data from being loaded. If you return false
from afterLoad hook, then record which we just loaded will be instantly unloaded.
This can be helpful in some cases.
This can be helpful in some cases, although you should still use :php:meth:`Model::addCondition`
where possible as it is much more efficient.

Insert/Update Hooks
-------------------

Insert/Update are triggered from inside save() method but are based on current state of
:php:meth:`Model::loaded`:

- beforeInsert($m, &$data) (creating new records only)
- afterInsert($m, $id)
- beforeUpdate($m, &$data) (updating existing records only. Not executed if model is not dirty)
- afterUpdate($m)

The $data argument will contain array of actual data (field=>value) to be saved, which you can
use to withdraw certain fields from actually being saved into the database (by unsetting it's value).

afterInsert will receive either $id of new record or null if model couldn't procude ID field. Also,
afterInsert is actually called before :php:meth:`Model::_reload_after_save` reloading is done.

For some examples, see :ref:`soft_delete`

beforeSave, afterSave Hook
--------------------------

A good place to hook is beforeSave as it will be fired when adding new records or
modifying existing ones:

- beforeSave($m) (saving existing or new records. Not executed if model is not dirty)
- afterSave($m) (same as above)

You might consider "save" to be a higher level hook, as beforeSave is called pretty
early on during saving the record and afterSave is caled at the very end of save.

You may actually drop validation exception inside save, insert or update hooks::

$m->addHook('beforeSave', function($m) {
if ($m['name'] = 'Yagi') {
throw new \atk4\data\ValidationException(['name'=>"We don't serve like you"]);
}
});
Loading, Deleting
-----------------

Those are relatively simple hooks:

- beforeLoad($m, $id) ($m will be unloaded). Break for custom load or skip.
- afterLoad($m). ($m will contain data). Break to unload and skip.

For the deletion it's pretty similar:

- beforeDelete($m, $id). Unload and Break to preserve record.
- afterDelete($m, $id).

A good place to clean-up delete related records would be inside afterDelete, although
if your database consistency requires those related records to be cleaned up first, use
beforeDelete instead.

For some examples, see :ref:`soft_delete`

Persistence Hooks
=================

Persistence has a few spots which it actually executes through $model->hook(), so
depending on where you save the data, there are some more hooks available.

Persistence_SQL
---------------

Those hooks can be used to affect queries before they are executed. None of these
are breakable:

- beforeUpdateQuery($m, $dsql_query)
- afterUpdateQuery($m, $statement). Executed before retrieving data.
- beforeInsertQUery($m, $dsql_query)
- afterInsertQuery($m, $statement). Executed before retrieving data.

The delete has only "before" hook:

- beforeDeleteQuery($m, $dsql_query)

Finally for queries there is hook ``initSelectQuery($m, $query, $type)``. It can be used to enhance queries
generated by "action" for:

- "count"
- "update"
- "delete"
- "select"
- "field"
- "fx" or "fx0"

Other Hooks:
============


More on hooks
===========
.. todo: The following hooks need documentation:
Coming soon
- onlyFields
- normalize
- afterAdd
6 changes: 3 additions & 3 deletions docs/model.rst
@@ -1,9 +1,9 @@

.. _Model:

============================
Working With Business Models
============================
=====
Model
=====

.. note:: This documentation needs to be reworked to be easier to read!

Expand Down
8 changes: 8 additions & 0 deletions src/Reference_One.php
Expand Up @@ -86,6 +86,13 @@ class Reference_One extends Reference
*/
public $mandatory = false;

/**
* Is field required? By default fields are not required.
*
* @var bool|string
*/
public $required = false;

/**
* Should we use typecasting when saving/loading data to/from persistence.
*
Expand Down Expand Up @@ -164,6 +171,7 @@ public function init()
'caption' => $this->caption,
'ui' => $this->ui,
'mandatory' => $this->mandatory,
'required' => $this->required,
'typecast' => $this->typecast,
'serialize' => $this->serialize,
'persist_format' => $this->persist_format,
Expand Down

0 comments on commit bd65c96

Please sign in to comment.