Introduction
Read and write database tables with
?placeholders
Fegg\Model builds SQL with ? placeholders on top of Fegg\Database\Pdo. It is the normal way for a controller to read and write the database. SQLite is the default; the connection is set in site/config/database_config.php (see Configuration).
The Fegg 4 class name Model still works through a compatibility layer, but every use is reported as Fegg deprecated: ... in the PHP error log, and it will be removed in Fegg 6. Write use Fegg\Model; at the top of the controller.
How to use
<?php
namespace Site\Controller;
defined('FEGG_CORE_DIR') || exit;
use Fegg\Model;
class News extends Controller
{
public function __init()
{
parent::__init();
$this->_model = new Model(false, array(
'table' => 'cms_news',
'id' => 'news_id',
'visible' => 'visible',
));
}
public function index()
{
$this->page['data'] = $this->_model
->where('category = ?', 2)
->order('post_date', 'desc')
->listMax(20)
->current($this->in('p'))
->find();
$this->page['pager'] = $this->_model->getPager();
$this->displayPage('news');
}
}
Tables
Tables are defined in YAML: htdocs/_fegg/site/config/database/*.yaml (project) and htdocs/_fegg/core/config/database/*.yaml (CMS core tables). ./fegg-cli migration applies them to the database.
Use ./fegg-cli db:schema to see the tables and columns (./fegg-cli db:schema cms_news for one table, --json for JSON). It reads the YAML files, so no database is needed, and it warns about tables that lack the columns below.
Required columns
Every table used with Fegg\Model needs these columns. The model fills them by itself (see core/config/database_regular_use_query.php):
| Column | Type | Set by |
|---|---|---|
valid | BOOLEAN | 1 on insert, 0 by delete. Reads only return rows with valid = 1 |
created | DATETIME | insert |
modified | DATETIME | insert and update |
Creating a model
$model = new Model(false, array(
'table' => 'cms_news', // table name
'id' => 'news_id', // primary key
'visible' => 'visible', // optional: public flag column
'bind' => array('category' => 'INTEGER', 'post_date' => 'DATETIME'), // casts on write
));
- The first argument is
$is_admin. Withfalse, reads only return rows whosevisiblecolumn is 1 (public pages). The CMS usestrueto see unpublished rows. See Model::__construct. bindtypes areINTEGER,FLOAT,DATEandDATETIME(case-insensitive). Any other type throwsInvalidArgumentException. See Model::addBindParam.- Table and column names (
table,id,visible, and the names given toorder(),join(),whereIn()and the other helpers) must be plain identifiers: letters, digits and_, optionallytable.column(news_id,cms_news.post_date). Anything else throwsInvalidArgumentException. Never build names from user input.
Reading
$rows = $model
->where('category = ?', 2)
->whereLike('title', 'news') // contains; % and _ match literally
->whereIn('news_id', array(1, 2, 3))
->order('post_date', 'desc')
->listMax(20)->current($this->in('p')) // page size and page number
->find(); // array of rows (associative arrays)
$pager = $model->getPager(); // current_page, last_page, number_of_records ...
$rows = $model->order('post_date', 'desc')->limit(5)->all(); // no paging
$row = $model->id($id); // one row by primary key, or null
$row = $model->where('title = ?', $title)->one(); // first match, or null
$title = $model->where('news_id = ?', $id)->value('title'); // one value, or null
$ids = $model->where('category = ?', 2)->column('news_id'); // array(3, 5, ...)
$count = $model->where('category = ?', 2)->count(); // int
$found = $model->where('title = ?', $title)->exists(); // bool
| Method | Returns |
|---|---|
| find | One page of rows; getPager has the links |
| all | Every matching row (limit caps it) |
| one / id | The first row, or null |
| value | The first column of the first row, or null |
| column | One column of every row, optionally keyed by another column |
| count | Number of rows (of groups, with group()) |
| exists | true when a row matches |
| toSql | The SELECT all() would run, with the values filled in; runs nothing |
find() or all()
find() pages: set the page size with listMax and the page with current, then read the links with getPager(). all() does not page; limit caps it. Do not use listMax(9999) to get everything, and do not combine limit() with find() (it throws LogicException).
Conditions are reset after every query
where(), order(), select(), limit() and the other chain methods only apply to the next query. After every query (find(), all(), one(), count(), insertRow() ...), failed ones included, the model forgets them, so each query starts clean. Model::reset does the same by hand.
The write methods (updateRowsWhere, deleteWhere ...) take their condition as arguments. Conditions chained with where() are not used by them (and are cleared).
Writing
$id = $model->insertRow(array('title' => $title, 'category' => 2, 'content' => null));
$n = $model->updateRow(array('title' => $title), $id); // rows updated
$n = $model->updateRowsWhere(array('visible' => 0), 'category = ?', 3);
$ids = $model->insertRows(array($row1, $row2)); // one transaction: all rows or none
$n = $model->delete($id); // soft delete: valid = 0
$n = $model->erase($id); // removes the row
- insertRow, updateRow and updateRowsWhere write only the given keys;
nullis stored as NULL. Do not passcreated,modifiedorvalidunless you mean to override them. - Update and delete methods return the number of affected rows.
- delete and deleteWhere are soft deletes (
valid = 0; reads no longer see the row). erase and eraseWhere remove the row. updateWhere(),deleteWhere(),eraseWhere()andupdateRowsWhere()refuse an empty condition. Pass'1 = 1'to really affect every row.- The older insert and update still work, but they write
''for every listed column missing from the data and cannot store NULL. - transaction runs several writes, of any number of models, all or nothing.
Errors
A failed query or connection throws Fegg\Database\QueryException. Its message ends with the SQL and its parameters ((SQL: Select ... Where no_such_column = 'x')); getSql() and getParameters() return them separately. Uncaught, it becomes an HTTP 500 and is written to the PHP error log and the application log. Fegg 4 printed the error and called exit instead.
Catch it only when the page can do something useful, for example with a duplicate key:
try {
$model->insertRow($row);
} catch (\Fegg\Database\QueryException $e) {
// ...
}
Wrong use of the builder (an array bound to <, an invalid column name, an update without a condition) throws InvalidArgumentException with a message saying what to do instead.
Debugging
toSql returns the SELECT a chain would run, without running it. debug prints the last query. Both fill in the values for reading.
echo $model->where('category = ?', 2)->order('post_date', 'desc')->toSql();
// Select * From "cms_news" Where category = 2 AND cms_news.visible = '1' And "cms_news".valid = 1 Order By post_date DESC
Raw SQL
For what the model cannot express, use Fegg\Database\Pdo directly. It shares the connection (and an open transaction) with the models. Raw queries get no automatic valid / created / modified handling.
Methods
Model::__construct
Model class constructor
Model::setTableName
Set Table Name
Model::getTableName
Get Table name
Model::setPrimary
Set Primary ID name
Model::getPrimary
Get Primary ID name
Model::setVisible
Set Visible flag name
Model::getVisible
Get Visible flag name
Model::setBindParam
Set Bind Parameter types
Model::addBindParam
Add Bind Parameter types
Model::getBindParam
Get Bind Parameter types
Model::select
Set Select Parameter
Model::where
Set Where Parameter
Model::whereOpen
Add open bracket on Where query
Model::whereClose
Add close bracket on Where query
Model::whereIn
Match a column against a list of values
Model::whereNotIn
Exclude a list of values
Model::whereLike
Search a column for a term
Model::whereNull
Match rows where a column is NULL
Model::whereNotNull
Match rows where a column is not NULL
Model::whereBetween
Match a range of values
Model::having
Set Having Parameter
Model::havingOpen
Add open bracket on Having query
Model::havingClose
Add close bracket on Having query
Model::order
Set Order Parameter
Model::orderRaw
Order by an SQL expression
Model::join
Set Join Parameter
Model::group
Set Group Parameter
Model::listMax
Set ListMax Parameter
Model::current
Set Current Page Parameter
Model::limit
Cap the number of rows
Model::find
Get one page of rows
Model::getPager
Get variables for making pager
Model::all
Get every matching row
Model::one
Get the first matching row
Model::id
Get one record by specified primary ID
Model::value
Get one value
Model::column
Get one column of every matching row
Model::count
Get the number of matching rows
Model::exists
Check whether a row matches
Model::toSql
Get the SELECT a chain would run
Model::insertRow
Insert a row from an associative array
Model::insertRows
Insert several rows in one transaction
Model::updateRow
Update a row from an associative array
Model::updateRowsWhere
Update the rows matching a condition from an associative array
Model::increment
Add to a numeric column
Model::insert
Execute Insert data
Model::update
Execute Update the record by specified primary ID
Model::updateWhere
Execute Update the records by specified condition
Model::delete
Execute Logical Delete the record by specified primary ID
Model::deleteWhere
Execute Logical Delete the records by specified condition
Model::erase
Execute Physical Delete the record by specified primary ID
Model::eraseWhere
Execute Physical Delete the records by specified condition
Model::transaction
Run queries all or nothing
Model::create
Execute Create Table
Model::dropTable
Execute Drop table
Model::dropIndex
Execute Drop index
Model::alterAddColumn
Execute Alter Add Column
Model::alterRenameTo
Execute Alter Rename
Model::pragma
Execute Pragma
Model::reset
Reset Model
Model::debug
Print the last executed query
Model::unsetRegularUseQuery
Skip the automatic queries for the next query
Model::unsetRegularUseQueryForTable
Skip the automatic queries of the table for the next query
Model::disabledRegularUseQuery
Skip the automatic queries for good