What is Model

Model is an implementation of repository pattern in Windwalker, it maintains some low-layer DB handler like DataMapper or ActiveRecord and help us do more database operation logic.

ModelRepository

ModelRepository is a very simple class without and DB access. This is an example of using it:

use Windwalker\Core\Model\ModelRepository;

class MyModel extends ModelRepository
{
    public function getItem()
    {
        return file_get_contents(__DIR__ . '/data.md');
    }

    public function save($content)
    {
        file_put_contents(__DIR__ . '/data.md', $content);
    }
}
// In controller
$model = new MyModel;

$item = $model->getItem();

// Do something...

$model->save($item);

Set source to ModelRepository:

// FileFinder is just an example
$model = new MyModel(null, null, new FileFinder);

// In model
$this->source->find(...);

Database Model

We can use DatabaseModelRepository to operate Database, here is a CRUD example, db is preset in Model so we can get it quickly:

use Windwalker\Core\Model\DatabaseModelRepository;

class FlowerModel extends DatabaseModelRepository
{
    public function getItem($id)
    {
        $query = $this->db->getQuery(true);

        $query->select('*')
            ->from('flowers')
            ->where('id = ' . $id);

        return $this->db->setQuery($query)->loadOne();
    }

    public function save($data)
    {
        if ($data->id)
        {
            $this->db->getWriter()->updateOne('flowers', $data, 'id');
        }
        else
        {
            $this->db->getWriter()->insertOne('flowers', $data);
        }
    }

    public function delete($id)
    {
        $query = $this->db->getQuery(true);

        $query->delete('flowers')
            ->where('id = ' . $id);

        return $this->db->setQuery($query)->execute();
    }
}

Use DataMapper

DataMapper is a easy way to help us operate database:

use Windwalker\Core\Model\DatabaseModelRepository;

class FlowerModel extends DatabaseModelRepository
{
    // Add table to make sure DataMapper use corrent DB table.
    protected $table = 'flowers';

    // Keys is optional since Model will set ['id'] as default
    // But you can override id if you use `uuid` or multiple primary key.
    protected $keys = 'id';

    public function getItem($id)
    {
        return $this->getDataMapper()->findOne($id);
    }

    public function save($data)
    {
        return $this->getDataMapper()->saveOne($data);
    }

    public function delete($id)
    {
        return $this->getDataMapper()->delete($id);
    }
}

See DataMapper

Use Record

You can also use ActiveRecord to handle data saving.

use Windwalker\Core\Model\DatabaseModelRepository;

class FlowerModel extends DatabaseModelRepository
{
    // Add table to make sure Record use corrent DB table.
    protected $table = 'flowers';

    // Keys is optional since Model will set ['id'] as default
    // But you can override id if you use `uuid` or multiple primary key.
    protected $keys = 'id';

    public function getItem($id)
    {
        $record = $this->getRecord();
        $record->load($id);  // @throws NoResultException;

        return $record->dump();
    }

    public function save($data)
    {
        $record = $this->getRecord();
        $record->bind($data)
            ->validate(); // @throws ValidateFailException;
            ->store();

        return true;
    }

    public function delete($id)
    {
        $this->getRecord()->delete($id);

        return true;
    }
}

See ActiveRecord

Magic Method

Model support a usage similar to NullObject pattern, if we call some method start with get*() or load*(), and this method not exists, Model will not raise error but only return null.

use Windwalker\Core\Model\Model;

// This is default model, does not have any custom methods
$model = new Model;

// These 2 methods will only return null
$data = $model->getData();

$list = $model->loadList();

So, we can use default Model to provide empty data for some object but won't breaking our program.

Model State

Windwalker Model is stateful design, use state pattern can help ue create flexible data provider. For example, we can change this state to get different data.

class MyModel extends DatabaseModel
{
    // ...

    public function getUsers()
    {
        $published = $this->state->get('where.published', 1);

        $ordering  = $this->state->get('list.ordering', 'id');
        $direction = $this->state->get('list.direction', 'ASC');

        $sql = "SELECT * FROM users " .
            " WHERE published = " . $published .
            " ORDER BY " . $ordering . " " . $direction;

        try
        {
            return $this->db->setQuery($sql)->loadAll();
        }
        catch (\Exception $e)
        {
            $this->state->set('error', $e->getMessage());

            return false;
        }
    }
}

$model = new MyModel;

$state = $model->getState();

// Let's change model state
$state->set('where.published', 1);
$state->set('list.ordering', 'birth');
$state->set('list.direction', 'DESC');

$users = $model->getUsers();

if (!$users)
{
    $error = $state->get('error');
}
Simple Way to Access State

Using get() and set()

// Same as getState()->get();
$model->get('where.author', 5);

// Same as getState()->set();
$model->set('list.ordering', 'RAND()');
State ArrayAccess
// Same as getState()->get();
$data = $model['list.ordering'];

// Same as getState()->set();
$model['list.ordering'] = 'created_time';

Model Caching

Windwalker Model provides runtime cache interface help us cache data in Model itself (This runtime cache only life in once page load, will not exists in next page loading, and won't affected by global configuration).

This is an example to use cache in Model:

use Windwalker\Core\Model\Model;

class MyModel extends Model
{
    public function getData()
    {
        if ($this->cache->exists('item.data'))
        {
            return $this->cache->get('item.data');
        }

        $data = file_get_contents(__DIR__ . '/data.md');

        $this->cache->set('item.data', $data);

        return $data;
    }
}

Generate Cache id When State Changed

Model state is dynamic, so if we change state, the cache key should be refresh that we can make sure we get same data when state is same, but get new data if state is changed.

public function getData()
{
    // Will generate a id look like: d967f4557f17dd542ece0f8a7b57b4f697c9b189
    $id = $this->getCacheId('item.data');

    if ($this->cache->exists($id))
    {
        return $this->cache->get($id);
    }

    $data = file_get_contents(__DIR__ . '/' . $this->state->get('file.name'));

    $this->cache->set($id, $data);

    return $data;
}

Custom Cache id Rule

If you trace getCacheId() at the parent, you will see:

public function getCacheId($id = null)
{
    $id = $id . json_encode($this->state->toArray());

    return sha1($id);
}

So override cache id rule is very easy, we can add some custom elements to hash:

public function getCacheId($id = null)
{
    $id .= json_encode($this->get('query.filter'));
    $id .= json_encode($this->get('query.search'));
    $id .= json_encode($this->get('query.where'));
    $id .= json_encode($this->get('query.having'));
    $id .= json_encode($this->get('query.ordering'));
    $id .= json_encode($this->get('query.direction'));
    $id .= json_encode($this->get('query.limit'));
    $id .= json_encode($this->get('query.start'));

    return sha1($id);
}

Use Callback

There is a simple way to quickly use cache, fetch() will auto check the cache exists or not and execute the callback to get data:

public function getData()
{
    $callback = function()
    {
        return file_get_contents(__DIR__ . '/' . $this->state->get('file.name'));
    };

    return $this->fetch('item.data', $callback);
}

If you found a typo or error, please help us improve this document.