https://github.com/bhallstein/tinymodel
An easy-peasy way to define a Model and store data using MySQL [legacy]
https://github.com/bhallstein/tinymodel
Last synced: about 1 year ago
JSON representation
An easy-peasy way to define a Model and store data using MySQL [legacy]
- Host: GitHub
- URL: https://github.com/bhallstein/tinymodel
- Owner: bhallstein
- Created: 2020-05-05T19:10:49.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2020-05-05T19:11:12.000Z (over 6 years ago)
- Last Synced: 2025-03-14T13:44:16.279Z (over 1 year ago)
- Language: PHP
- Size: 68.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: Readme.md
Awesome Lists containing this project
README
# TinyModel
TinyModel is a PHP superclass for defining the Model layer of your web application. It handles the communication between application and database, and returns friendly nested objects, even for arbitrary joins.
## Examples
### Example 1: Authenticating a user
*MyModel.php:*
require('TinyModel.php');
class User extends TinyModel {
const userid = 'int';
const username = 'varchar alphanumeric maxlength=30 notnull';
const password = 'char maxlength=60 notnull';
const email = 'varchar email maxlength=100';
}
class Favourite extends TinyModel {
const faveid = 'int';
const userid = 'int not null';
const itemid = 'int not null';
}
class Item extends TinyModel {
const itemid = 'int';
const name = 'varchar maxlength=40 notnull';
}
*login.php:*
require('MyModel.php');
$p_username = $_POST['username'];
$p_password = $_POST['password'];
$res = User::fetch(
new Condition('username', $p_username)
);
if ($res->status !== TMResult::Success || count($res->result) != 1) {
invokeView('loginError', $res);
}
else {
$db_pwd = $res->result[0]->password;
$authenticated = password_verify($p_password, $db_pwd);
if ($authenticated)
invokeView('loginSucceeded', $res);
else
invokeView('loginError', $res);
}
### Example 2: Fetching with joins
TinyModel extracts all results into objects of the classes you create. If there are joins, it extracts joined results into sub-objects, attaching an array of them to the parent object. TinyModel can perform this traversal recursively:
require('MyModel.php');
$conn = new PDO('mysql:host=127.0.0.1;charset=utf8;...');
TinyModel::setConnection($conn);
$res = User::fetch(
new Condition('userid', $p_userid),
new Join('Favourite', 'userid', new Join('Item', 'itemid))
);
if ($res->status === TMResult::Success) {
echo json_encode($res->result, JSON_PRETTY_PRINT);
}
This might output:
[
{
"userid": 269,
"username": "someone",
"email": "someone@somewhere.com",
"password": "~",
"favourites": [
{
"faveid": 220,
"userid": 269,
"item": 241,
"items": [
{
"itemid": 241,
"name": "tshirt"
}
]
}
]
}
]
## Defining tables
To configure TinyModel, you define a set of subclasses, each representing a table in your database. Class constants are then used to specify the name and type of the columns in the table, and optional restrictions on values.
### Class & table names
The name of the table must be the (lowercase) plural of the name of the class. e.g. For a table `users`, create a class called `User`. TinyModel pluralises the class name to find the table name (but bear in mind it doesn’t know about invariant words such as 'Sheep').
### Column specification
To define a column, you create a class constant with the same name as the column, and initialize it with a *column specification string*, with the format `"type [restrictions]"`:
- type: one of the following: `int`, `float`, `char/varchar` (these are equivalent), `text`, `timestamp`
- restrictions: one or more of: `alphabetical`, `alphanumeric`, `email`, `url`, `positive`, `notnull`, `maxlength=N`
**Note on notnull:** ID columns are generally "not null" in the database, but should *not* be specified as such in TinyModel column specification. This is because `insert()` and `update()` must allow null values for ID columns.
### Example of a class
If you have the tables `users` and `items`, you might create two classes as follows:
class User extends TinyModel {
const userid = 'int';
const username = 'varchar alphanumeric maxlength=20 notnull';
const email = 'varchar email notnull';
}
class Items extends TinyModel {
const itemid = 'int';
const userid = 'int notnull';
const itemname = 'varchar maxlength=20 notnull';
}
TinyModel then allows you to interact with these tables via the `fetch`, `update` and `insert` methods.
## TMResult
All methods return a TMResult object, returning the status of the query and any returned data.
Fields:
- **status:** one of the following statuses:
- *TMResult::Success*
- *InvalidData* – update or insert data failed validation
- *InvalidConditions* – condition(s) failed validation
- *InternalError* – TinyModel encountered an error executing the query
- **result:** returned data appropriate to the query:
- *fetch:* an array of fetched objects
- *update:* the number of rows updated
- *insert:* the insert ID
- **errors:** error data appropriate to the query and type of failure:
- *InvalidConditions:*
- if there were *no* conditions, `errors` will be null
- if some conditions did not pass validation, an array of errors in the form `column_name => validation_error` (see list of validation errors below)
- *InvalidData:*
- if some updates/inserts did not pass validation_error, an array of errors in the form `column_name => validation_error`
- *InternalError:*
- `errors` is set to the result of calling `errorInfo()` on the failing PDO statement
**Validation Errors:** these specify the type of error that was encountered when validating insert/update data or a condition:
- *NonexistentColumn:* the column does not exist in the table specification
- *InvalidValue:* the value failed column restrictions
- *UnknownObject:* a non-Condition object was passed where a Condition object was expected
## Methods
TinyModel’s three public methods all return a TMResult object encapsulating query status (success/failure), and returned data or errors.
### fetch
`fetch($conditions, $joins, $debug = false)`
Static method. Attempt to fetch items from the database.
*conditions*
A Condition object, or an array of Condition objects, to specify what to fetch. For instance, to only return rows where the userid is 76:
new Condition('userid', 76)
You can specify multiple nested conditions, and the relationships between them (`and` or `or`), by passing a nested array. For details, see Conditions. At least one valid Condition object is required for all queries.
*joins*
An optional Join object, or an array of Join objects.
*debug*
If true, prints out the generated `select` query before executing it.
*Example:* Fetch user(s) with id 76, joined to any favourites, joined to the favourited items:
User::fetch(
new Condition('userid', 76),
new Join('Favourite', 'userid', new Join('Thing', 'thingid')))
);
### update
`update($updates, $conditions, $debug = false)`
Static method. Updates objects matching the given condition(s).
*updates*
An associative array of updates to perform, with the key specifying the column, and the value the new entry for that column.
*conditions*
A Condition object or an array of Condition objects, governing which rows in the table will be updated with new value(s).
*debug*
If true, prints out the query before executing it.
*Example:* Update username of the user with id 12 to ‘jimmy’:
User::update(
array('username' => 'jimmy'),
new Condition('userid', 12)
);
### insert
`insert()`
Instance method. Insert an object into its corresponding table. You first create an instance, filling out its properties, then simply call `insert()` on it.
*Example:* Insert a favourite for Jimmy into the `favourites` table.
$f = new Favourite;
$f->userid = 12;
$f->thingid = $thingid;
$f->insert();
### Condition objects
Condition objects are used to control which rows should be fetched or updated. The constructor takes the following values:
- *column:* The name of the column the condition applies to
- *value:* The value to test against
- *test:* One of the following constants. The default is Equals.
- `Condition::Equals`
- `NotEquals`
- `LessThan`
- `LessThanOrEquals`
- `GreaterThan`
- `GreaterThanOrEquals`
- `Recent`
- This is used to secify a timestamp column with a time value up to a certain amount of time in the past. When the `Recent` condition is used, the value field of the Condition specifies the number of seconds before the current timestamp that should be considered a match.
- *conjunction:* You can pass a nested array of Conditions to the `fetch` or `update` methods, allowing you to specify a set of conditions such as `where userid < 76 or (email = 'a@b.com' and username = 'geoff')`. The `conjunction` parameter specifies the conjunction with which the *following* condition or array of conditions relates to the current one: ‘or’ or ‘and’.
i.e. The aforementioned nested set of conditions would be represented as follows:
User::fetch(
array(
new Condition('userid', 76, Condition::LessThan, Condition::_Or),
array(
new Condition('email', 'a@b.com'),
new Condition('username', 'geoff')
)
)
);
The default conjunction is `Condition::_And`.
### Join objects
You can pass a Join object or an array of Join objects to a call to `fetch`, to join the results returned from this table to other tables.
The constructor has the arguments:
- *class:* The name of the class (*not* the table name). For instance, `'User'`.
- *columns:* The name of the column(s) on which to join. You can pass:
- a string – join tables on a single column
- an array of strings – join on several columns
- an associative array – the key specifies the home column, and the value specified the away column
- *joins:* Optionally, a further Join (or array of Joins), from the away table to (an)other table(s).
When more than one column is provided for a join, *all* specified columns’ values must be the same in both tables for the row to be joined. i.e. the generated query is as follows:
new Join(Thing, ['A', 'B'])
–> ...left join things on users.A = things.A and users.B = things.B
### Password columns
In previous versions, applying a condition to a column named 'password' was handled separately – the supplied test value was concatenated with an assumed `salt` column, then SHA’d and then MD5’d, and the result tested against the stored value.
This has changed in TinyModel 0.91. Set password fields as ordinary `char` or `varchar` columns, and use PHP’s new `password_hash()` and `password_verify()` functions to generate values/test stored values against authentication attempts. (These functions use the bcrypt algorithm, and automatically incorporate a per-user salt.)
### Length restrictions & multibyte characters
It’s very good practice in the modern world to set the charset of tables to UTF8. Indeed, TinyModel considers this an assumption. But, note that:
- For char/varchar columns, as of MySQL 5.1, with a length of N, you can store N *characters* in the field.
- For text types, you have a byte limit, so the number of characters that can be stored is variable:
- tinytext: 255 bytes
- text: 65,535 bytes
- mediumtext: 16,777,215 bytes
- longtext: 4,294,967,295 bytes
i.e. the maxlength restriction for `char` or `varchar` columns will count characters, whereas for `text` columns it will count bytes.
## Usage in a web application
For a simple web app, you might typically define your TinyModel subclasses in a single file, thereby specifying the tables you wish to communicate with.
The controller layer includes this file, and can then interact with the subclasses, making calls to TinyModel’s `fetch`, `update` and `insert` methods.
For a functioning example, see the file `demo.php`. (You will need to need to create the relevant database and tables.)
## License
TinyModel is published under the MIT license, and comes with no warranty whatsoever.
© Ben Hallstein