Back in 2010, I was building a hosted VOIP PBX that required a LOT of calls to the database. I wanted to make it as simple as I could without having to import a huge ORM (you know which ones I’m talking about). PicoORM is a very tiny (10.2kb) library for PHP 7.1 and above that provides an easy way of working with databases by representing table columns as properties of an object. It provides complete developer control and doesn’t get in the way of rapid development.
Obtaining PicoORM
- Clone it from GitHub at https://github.com/paigejulianne/picoorm.git and require it at the top of your PHP scripts
- Add it via composer using composer require paigejulianne/picoorm within your project
Example Database Used Throughout this Post
This SQL can be imported directly into an existing MySQL/MariaDB database to rebuild the structure.
CREATE TABLE `user` (
`id` bigint(20) UNSIGNED NOT NULL,
`email` varchar(255) NOT NULL,
`password_hash` varchar(255) NOT NULL,
`acl` tinyint(3) UNSIGNED DEFAULT 0
);
--
-- Indexes for table `user`
--
ALTER TABLE `user`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for table `user`
--
ALTER TABLE `user`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;
Setting Up Your Connection
PicoORM relies heavily on PDO and its database drivers, which must be installed and working correctly on your system. The global variable _PICO_PDO is simply a PDO object. The simplest example, which connects to a MySQL or MariaDB database is:
$GLOBALS['_PICO_PDO'] = new PDO("mysql:host=localhost;db_name=database", "username", "password");
The exact PDO connection string will depend on which database you are using. See a list of PDO Database drivers and find their connection string at https://www.php.net/manual/en/pdo.drivers.php
Inheriting the PicoORM Class
For each table that you wish to use PicoPDO with, you must create a new class that extends the base PicoORM class. This is as simple as::
class User extends PicoORM {
// custom code here
}
- Your class name must match the name of your table. In MySQL/MariaDB, table names are case-insensitive.
- You do not necessarily need an id or autonumbered field in your table.
Creating a Record
Say we want to create a new user in our user table. Using the User
class, we can do the following:
$email = "example@paigejulianne.com";
$password_hash = password_hash("example");
$newUser = new User();
$newUser->email = $email;
$newUser->password_hash = $password_hash;
$newUser->save();
Alternatively, you can set fields dynamically and all at once by using the method setMulti
$newUser = new User();
$newUser->setMulti(['email' => $email, 'password_hash' => $password_hash]);
Checking to Ensure a Record Exists
There are two ways to check to see if a record exists in the database. The first way uses the static method exists
. The other attempts to create the object. If you use the latter method, you’ll need to check to see if one of the column fields is set.
// returns a simple true/false if the record exists in the table
$userExists = User::exists($email, "email");
// returns an object based on email and check that the record has an `id` column that is set to *something*
$user = new User($email, "email");
if (isset($user->id)) {
// true
} else {
// false
}
Retrieving a Record
As you saw in the previous example, retrieving a record is simple. If your table contains a column named id
, you can simply pass the value in the object constructor. However, if you want to retrieve a record on any other field, you can pass the column name as the second parameter to the constructor.
// fetch a user based on `id` column
$user = new User(1);
// fetch a user based on `email` column
$user = new User("example@paigejulianne.com", "email");
Retrieving Multiple Records
You can choose to retrieve multiple objects by using the getAllObjects
and pass it multiple “filters”. For example, to filter based on the column acl
, you can do the following:
$filter = [ ['acl', '>=', 0] ];
$allUsers = User::getAllObjects('id', $filter);
If only one-row matches, you’ll get a single User
object. But, normally this will return an array of User
objects.
Getters and Setters
As seen in our user creation script, setting the value of a column is as simple as setting an object property. So, retrieving a column value is just as easy.
$user = new User(1);
echo 'This user\'s email address is ', $user->email;
// changing email address
$user->email = "picoorm@paigejulianne.com";
Writing Changes to the Database
By default, PicoORM doesn’t write changes to the database until you either call the save
method or the object is destroyed (for example, the script is ending). I recommend calling save
every time you set a field to prevent race conditions.
// explicit save
$user->email = "picoorm@paigejulianne.com";
$user->save();
// implicit save
$user->email = "example@paigejulianne.com";
die(); // at this point, the object is destroyed by PHP.
// However, under the covers, PicoORM calls the
// save() method to write changes to the database.
Implementing Your Own Functions
Your class can be extended to implement your own functions.
class User extends PicoORM {
// returns a boolean if the new user was created
public static function createNewUser($email, $password_plain, $acl = 0) {
if (!self::exists($email, 'email')) {
$newUser = new User();
$newUser->email = $email;
$newUser->password_hash = password_hash($password_plain);
$newUser->save();
return true;
} else {
return false;
}
}
// login the user and set a session variable if password matches and returns a boolean
public static function loginUser($email, $password_plain) {
$user = new User($email, 'email');
if (isset($user->id)) {
if (password_verify($password_plain, $user->password_hash)) {
$_SESSION['user_id'] = $user->id;
return true;
} else {
// password did not match
return false;
}
} else {
// user not found
return false;
}
}
}
Working Directly with SQL
Yes, you can work directly with SQL through PicoORM using three static methods, depending on your needs just as easily as working with the built-in methods. You can also use placeholders in your SQL like you would with a PDO statement (and strings will be properly escaped).
If you use the “magic” text _DB_
in your SQL statement, it will be replaced by the table name (which is based on the class name).
// fetch one row from the database
$email = "example@paigejulianne.com";
$sql = "SELECT * FROM _DB_ WHERE email = ?";
$resultArray = User::_fetch($sql, [$email]);
// fetch multiple rows from the database
$acl = 0;
$sql = "SELECT * FROM _DB_ WHERE acl >= ?";
$resultArray = User::_fetchAll($sql, [$acl]);
// fetch a PDOStatement to do what you want with it
$statement = User::_doQuery($sql, [$acl]);
You can also use the base class of PicoORM to access a database by passing a third, optional parameter to any of the above methods.
$email = "example@paigejulianne.com";
$sql = "SELECT * FROM _DB_ WHERE email = ?";
$resultArray = PicoORM::_fetch($sql, [$email], 'user');
Working with Other Databases
If for some reason, you have a table in a database that isn’t in the database you specified in your connection string (see the top of this tutorial), as long as the database user has access, you can still use PicoORM with it. Simply use the database name as the root name and the table name as the class name. For example:
// work with a table called `Transactions` in the `Log` database
class Log\Transactions extends PicoORM {
}
Bonus Content
If you made it this far, congratulations! Here’s a helper method that you might find useful. Characters that could be ambiguous (or hard to discern) are removed.
const ALLOWED_CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
const ALLOWED_CHAR_COUNT = 55;
public static function generateRandomString($length = 6): string {
for ($i = 0; $i < $length; $i++) {
$random .= self::ALLOWED_CHARS[rand(0, self::ALLOWED_CHAR_COUNT - 1)];
}
return $random;
}
Getting Help / Suggestions / Bug Reports / Feedback
For help and assistance, please use GitHub discussions, found at https://github.com/paigejulianne/picoorm/discussions
All suggestions, bug reports, or feedback must be submitted through GitHub at https://github.com/paigejulianne/picoorm/issues