What is Repository
Repository is a class which implemented the repository pattern in Windwalker, it contains some low-layer DB handler like DataMapper or ActiveRecord and help us do more database operation logic.
Repository
Repository
is a very simple class without any DB access. This is an example of using it:
use Windwalker\Core\Repository\Repository;
class MyRepository extends Repository
{
public function getItem()
{
return file_get_contents(__DIR__ . '/data.md');
}
public function save($content)
{
file_put_contents(__DIR__ . '/data.md', $content);
}
}
// In controller
$repo = new MyRepository();
$item = $repo->getItem();
// Do something...
$repo->save($item);
Set source to Repository:
// FileFinder is just an example
$repo = new MyRepository(null, null, new FileFinder());
// In repository
$this->source->find(...);
Database Repository
We can use DatabaseRepository
to operate Database, here is a CRUD example, db
is preset in Repository so we can get it quickly:
use Windwalker\Core\Repository\DatabaseRepository;
class FlowerRepository extends DatabaseRepository
{
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\Repository\DatabaseRepository;
class FlowerRepository extends DatabaseRepository
{
// Add table to make sure DataMapper use corrent DB table.
protected $table = 'flowers';
// Keys is optional since Repository 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\Repository\DatabaseRepository;
class FlowerRepository extends DatabaseRepository
{
// Add table to make sure Record use corrent DB table.
protected $table = 'flowers';
// Keys is optional since Repository 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
Repository support a usage similar to NullObject pattern, if we call some method start with get*()
or load*()
, and this method not exists,
Repository will not raise error but only return null
.
use Windwalker\Core\Repository\Repository;
// This is default repository, does not have any custom methods
$repo = new Repository();
// These 2 methods will only return null
$data = $repo->getData();
$list = $repo->loadList();
So, we can use default Repository to provide empty data for some object but won't breaking our program.
Repository State
Windwalker Repository 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 MyRepository extends DatabaseRepository
{
// ...
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;
}
}
}
$repo = new MyRepository();
$state = $repo->getState();
// Let's change repository state
$state->set('where.published', 1);
$state->set('list.ordering', 'birth');
$state->set('list.direction', 'DESC');
$users = $repo->getUsers();
if (!$users) {
$error = $state->get('error');
}
Simple Way to Access State
Using get()
and set()
// Same as getState()->get();
$repo->get('where.author', 5);
// Same as getState()->set();
$repo->set('list.ordering', 'RAND()');
State ArrayAccess
// Same as getState()->get();
$data = $repo['list.ordering'];
// Same as getState()->set();
$repo['list.ordering'] = 'created_time';
Repository Caching
Windwalker Repository provides runtime cache interface help us cache data in Repository 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 Repository:
use Windwalker\Core\Repository\Repository;
class MyRepository extends Repository
{
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
Repository 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.