PHP
Article

Getting Started with Medoo – Examples of Use

By Wern Ancheta

In this article I’m going to walk you through Medoo, a lightweight database abstraction library for PHP. Its main features include:

  • Support for multiple databases – it supports MySQL, MariaDB, Sybase, MS SQL, PostgreSQL, and Oracle.
  • Secure – prevents SQL injection, it uses PDO.
  • Easy to use – its API is very intuitive.

While Medoo is nothing revolutionary, and the fact that it sports a very small filesize matters little to few, it’s still an interesting project that went from being outright dismissed to vaguely accepted, as evident in these threads. It’s on its way up, and that’s our reason for taking a look at it.

medoo-logo-256

Installation

Even though the website recommends installing it by downloading a file and including it in your project, you should use Composer to do so.

Connecting to the Database

Creating a new instance of Medoo requires an array to be passed in as an argument. The array should contain the following items:

  • database_type – the type of database you want to connect to. This can either be one of the following: mysql, mariadb, pgsql, sybase, oracle, mssql, sqlite.
  • database_name – the name of the database.
  • server – the name of the server or IP address.
  • username – the username of the user used for logging in to the database.
  • password – the password of the user.
$db = new medoo(array(
    'database_type' => 'mysql',
    'database_name' => 'pokemon',
    'server' => 'localhost',
    'username' => 'ash_ketchum',
    'password' => 'pikachu'
));

The default port used by medoo is port 3306. If your database server uses something different, you can also pass in the port and assign the correct port as the value.

Selecting Data

The first thing we’re going to do is select data from the database. You can download the example database here if you want to follow along.

You can select data from a specific table using the select method. This takes up the following arguments:

  • name of table
  • join condition (optional)
  • fields to select
  • where condition (optional)

As a primer, let’s select some trainers from the database:

$trainers = $db->select(
    'trainers', 
    array('id', 'name', 'pokemon_count', 'region')
);

Medoo returns the results as an array:

Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Brock
            [pokemon_count] => 7
            [region] => Kanto
        )

    [1] => Array
        (
            [id] => 2
            [name] => Marshal
            [pokemon_count] => 8
            [region] => Unova
        )
...

The query above selects all the trainers. What if we only want to select trainers from a specific region? We can do that by specifying the where condition:

$trainers = $db->select(
    'trainers', 
    array('id', 'name', 'pokemon_count', 'region'), 
    array('region' => 'Kanto')
);

The query above only returns the trainers from the Kanto region.

What if we only want to select the trainers who have defeated a specific number of trainers from a specific region? Here’s how we do that:

$trainers = $db->select('trainers', 
    array('id', 'name', 'trainers_defeated', 'region'), 
    array('AND' => array('trainers_defeated[>]' => 150, 'region' => 'Johto'))
);

All you have to remember when using the where condition is to specify an array of fields with their corresponding condition as a value for the keyword that you want to use. In this case we want the condition for both to return true so we’re using the AND keyword.

If you need to use a relational operator other than =, you need to specify it after the name of the field and wrap it in brackets []. Here are some examples:

  • Select trainers who defeated more than 150 opponents:
'trainers_defeated[>]' => 150
  • Select trainers whose badges_count is not equal to zero:
'badges_count[!]' => 0
  • Select trainers who defeated trainers within the range of 100 to 300:
'trainers_defeated[<>]' => array(100, 300)
  • Select trainers whose badges_count is less than 8:
'badges_count[<]' => 8

Going back, what if we want to select the trainers who are from either ‘Kanto’ or ‘Unova’ and have a badges_count of 5 and above or trainers_defeated of 20 and above? Here’s how we do it:

$trainers = $db->select(
    'trainers',
    array('id', 'name', 'trainers_defeated', 'badges_count', 'region'),
    array('AND' => array(
        'region' => array('Kanto', 'Unova'),
        'OR' => array('badges_count[>=]' => 5, 'trainers_defeated[>=]' => 20)
    ))
);

All we had to do was to wrap our conditions inside the AND keyword. For the first condition we passed in an array. In this case, it’ s the name of regions that we want to select. In the second condition, we had to wrap it in OR, this means that the conditions inside it will return results for either of them.

Now we want to know which trainer has the highest number of Pokemon caught. We can do that by using the get method. Unlike the select method, this will only return a single row from the table that we are fetching from.

$most_caught = $db->get(
    'trainers', 
    array('name', 'pokemon_count'), 
    array('ORDER' => 'pokemon_count DESC')
);

You can also use ORDER with the select method. For example, we want to select all the trainers and order them by the number of Pokemon they caught.

$trainers_most_caught = $db->select(
    'trainers', 
    array('name', 'pokemon_count', 'trainers_defeated'), 
    array('ORDER' => 'pokemon_count DESC')
);

We can also order by multiple fields. Instead of passing in a string for the value of the ORDER, we pass in an array:

$trainers_most_caught = $db->select(
    'trainers', 
    array('name', 'pokemon_count', 'trainers_defeated'), 
    array('ORDER' => array('pokemon_count DESC', 'trainers_defeated DESC'))
);

What if we want to know how many trainers are in each region? Medoo doesn’t have that kind of functionality built into it yet, so we’ll have to use a raw query:

$trainer_count_per_region = $db->query(
    'SELECT COUNT(name) as trainers, region FROM trainers GROUP BY region'
    )->fetchAll(PDO::FETCH_ASSOC);

Behind the scenes, Medoo uses PDO’s fetch method. This means it also uses the default fetch style used by PDO’s fetch method. So we have to pass in the fetch style as an argument to the fetchAll method. In this case its PDO::FETCH_ASSOC, this means that it will only return the field names and their value. If we leave it blank, it will return both the associative and indexed columns. Here’s the result that we get when we use PDO::FETCH_ASSOC as the fetch style:

Array
(
    [0] => Array
        (
            [trainers] => 2
            [region] => Hoenn
        )

    [1] => Array
        (
            [trainers] => 4
            [region] => Johto
        )

    [2] => Array
        (
            [trainers] => 2
            [region] => Kalos
        )

    [3] => Array
        (
            [trainers] => 1
            [region] => Kanto
        )

    [4] => Array
        (
            [trainers] => 3
            [region] => Unova
        )

)

If we leave it blank, the results will look like this:

Array
(
    [0] => Array
        (
            [trainers] => 2
            [0] => 2
            [region] => Hoenn
            [1] => Hoenn
        )

    [1] => Array
        (
            [trainers] => 4
            [0] => 4
            [region] => Johto
            [1] => Johto
        )

    [2] => Array
        (
            [trainers] => 2
            [0] => 2
            [region] => Kalos
            [1] => Kalos
        )

    [3] => Array
        (
            [trainers] => 1
            [0] => 1
            [region] => Kanto
            [1] => Kanto
        )

    [4] => Array
        (
            [trainers] => 3
            [0] => 3
            [region] => Unova
            [1] => Unova
        )

)

Finally, let’s touch on joins. We can use the following syntax for joins:

  • > – left join
  • < – right join
  • >< – inner join
  • <> – full join

Here’s an example of using an inner join to select all pokemon and their corresponding type names:

$db->select(
    'pokemon', 
    array('[><]types' => array('type_id' => 'id')),
    array('pokemon.name', 'types.name(type)')
);

Just like with the regular select method, the first argument is the name of the table. In this case its the primary table we want to select data from. The second argument is an array containing the join condition. We supply the join type and the name of table to join as the key. And the value is an array with the field on the primary table as the key and the field on the secondary table as the value. The third argument is the array of fields that you want to return. Its good practice to always specify the name of the table before the field name. Currently medoo doesn’t support table aliases so we’ll have to use the full name of the table for now. If the name of the fields that you want to select are the same, you need to use a column alias. If you don’t, only the last field with the same name that you specified will get selected. In our example above we specified an alias by using this syntax:

table_name.field_name(alias)

Inserting Data

Next, we add some data into the pokemon table. For that, were going to use the Pokemon API as our data source.

First install guzzle with composer require guzzlehttp/guzzle:~5.0

Create a new instance of the guzzle client:

use GuzzleHttp\Client;

$client = new Client();

Next, we select all the trainers and the Pokemon types from the database. We will be using these later on when we insert the data into the pokemon table.

$trainers = $db->select('trainers', array('id', 'pokemon_count'));

$types = $db->select('types', array('id', 'name'));

Next, we make a request to the pokedex resource in the Pokemon API. This returns a JSON string, so we use guzzle’s json method to convert it to an array. The pokedex resource returns an array of all pokemon.

$pokedex_response = $client->get('http://pokeapi.co/api/v1/pokedex/1');
$pokedex_data = $pokedex_response->json();

$pokemon = $pokedex_data['pokemon'];
$total_pokemon = count($pokemon) - 1; //were zero-indexed

Next, we loop through all the trainers, get the number of Pokemon they caught and then create a for loop based on that number. The pokedex resource returns the name of the pokemon and a resource URI in which we can get further information about it. We also need to get the primary type of the pokemon and a list of moves that it can do so we also make a request to the pokemon resource. Once we get the data back, we then need to get the id of the pokemon type. We already have a table that has a list of all the pokemon types so we just determine the id by looping through it, and if the type name returned from the API is the same as one of the type names from the database, we just get its id. After that, we give the pokemon a random level by using mt_rand. Next, we call Medoo’s insert method to insert the Pokemon data in the database. This takes the name of the table as the first argument, then the array of data as the second argument. Finally, we assign some moves to each of the Pokemon. When you call the insert method, it returns the last insert id. We use this last insert id to determine the id of the pokemon that was assigned by the database, and then insert it to the pokemon_moves table along with the name of the move.

foreach($trainers as $trainer){
    $trainer_id = $trainer['id'];
    $pokemon_count = $trainer['pokemon_count'];

    for($x = 0; $x < $pokemon_count; $x++){
        
        $pokemon_id = mt_rand(0, $total_pokemon);
        $pokemon_name = $pokemon[$pokemon_id]['name'];
        $pokemon_resource = $pokemon[$pokemon_id]['resource_uri'];

        $pokemon_response = $client->get('http://pokeapi.co/' . $pokemon_resource);
        $pokemon_data = $pokemon_response->json();

        $pokemon_types = $pokemon_data['types'];

        //pokemon types in the database starts with a capital letter
        $type_name = ucfirst($pokemon_types[0]['name']); 

        $type_id = null;
        foreach($types as $t){ //determine type id
            if($t['name'] == $type_name){
                $type_id = $t['id'];
            }
        }

        $level = mt_rand(1, 100); //give a random level between 1 and 100

        $pokemon_db_id = $db->insert(
            'pokemon',
            array(
                'name' => $pokemon_name,
                'trainer_id' => $trainer_id,
                'type_id' => $type_id,
                'level' => $level 
            )
        );

        //assign some moves
        $pokemon_moves = $pokemon_data['moves'];
        if(!empty($pokemon_moves)){
            $move_count = count($pokemon_moves) - 1;
            $move_limit = 4; //each pokemon can only have 4 moves
            for($z = 0; $z < $move_limit; $z++){
                $move_id = mt_rand(0, $move_count); 
                $move_name = $pokemon_moves[$move_id]['name'];
                
                $db->insert(
                    'pokemon_moves', 
                    array(
                        'pokemon_id' => $pokemon_db_id, 
                        'move_name' => $move_name
                    )
                );
            }
        }
         
    }
}

After running the script above, we should now have some Pokemon along with their moves in our database.

Let’s say trainer Roxie caught 3 new Pokemon: Drapion, Toxicroak and Crobat. How can we insert them all at once into our pokemon table? The insert method also supports multi-inserts, so we can just supply an array containing all of the rows of data that we want to insert.

First, we get the trainer id from the trainers table:

$trainer_id = $db->get('trainers', 'id',  array('name' => 'Roxie'));

Let’s assume that those 3 Pokemon have the same type so we go ahead and get the type id from the database:

$type_id = $db->get('types', 'id', array('name' => 'Poison'));

The pokemon API doesn’t really have a search method, so all we can do is access the pokedex resource directly from the browser:

http://pokeapi.co/api/v1/pokedex/1/

And then look for the resource_uri that we want to get. I already did it so you don’t have to. The ids that we need are: 452, 454 and 169. We then loop through those and get all the necessary data. This time, instead of doing an insert call on every iteration of the loop, we store it in an array. We then call the insert method once and supply the array in which we stored the pokemon data.

$ids = array(452, 454, 169);

$pokemon_caught = array();
foreach($ids as $id){

    $response = $client->get('http://pokeapi.co/api/v1/pokemon/' . $id);
    $data = $response->json();

    $name = $data['name'];

    $pokemon_caught[] = array(
        'name' => $name,
        'trainer_id' => $trainer_id,
        'type_id' => $type_id,
        'level' => mt_rand(1, 100)
    );

}

$db->insert('pokemon', $pokemon_caught);

Updating Data

Now Roxie has 3 more Pokemon, but her trainer data hasn’t been updated yet. We need to add 3 to her current pokemon_count. To do that we call the update method. This takes up the following arguments:

  • name of table
  • data
  • where condition

Here’s how we do an update:

$db->update(
    'trainers', 
    array('pokemon_count[+]' => 3), 
    array('id' => $trainer_id)
);

See what we did there? Medoo comes with a nice utility wherein you can do the following mathematical operations on a specific field that you’re updating:

  • + – add a specific value to the current value.
  • - – subtract a specific value to the current value.
  • * – multiply a specific value to the current value.
  • / – divide a specific value to the current value.

Deleting Data

Now we want to release Drapion because it sucks. We can delete him from the pokemon table using the delete method. This takes up the name of the table as its first argument and an array of conditions as the second argument.

$db->delete('pokemon', array('name' => 'Drapion'));

We can also delete based on 2 conditions. Here we want to delete all Pokemon which are of ‘Normal’ type and are below level 60:

$type_id = $db->get('types', 'id', array('name' => 'Normal'));
$db->delete(
    'pokemon', 
    array('AND' => array('level[<]' => 60, 'type_id' => $type_id))
);

Note that calling delete doesn’t return the id of the deleted row. This means you’ll have to find a way to get it on your own if you want to use that id for something.

Aggregate Functions

Medoo also comes with some aggregate functions.

For example, we want to get the total number of trainers from the trainers table. For that, we use the count method:

$total_trainers = $db->count('trainers');

If we want to know the trainer with the most/least number of Pokemon, there’s also a function for that. For getting the most, we use the max method. This takes up the name of the table and the field you want to use:

$db->max('trainers', 'pokemon_count');

For getting the least, we use the min method:

$db->min('trainers', 'pokemon_count');

We can also use the avg method, if we want to know the average number of pokemon that each trainer in the trainers table have:

$db->avg('trainers', 'pokemon_count');

We can also get the total number of pokemon that all of the trainers have by using the sum method:

$db->sum('trainers', 'pokemon_count');

Note that with the count, min, max, and sum method we can also specify some additional conditions. We supply those as the last argument. For example, we only want to know the maximum number of pokemon that a trainer has in the ‘Hoenn’ region:

$db->max('trainers', 'pokemon_count', array('region' => 'Hoenn'));

Debugging

Medoo also provides utilities for checking errors and debugging. Note that medoo doesn’t return any errors when you try to do something that cannot yield a successful result, that’s why sometimes you have to check for it explicitly.

For example, we made a typo for the trainers table. Instead of trainers we typed in trainerw:

$db->update(
    'trainerw', 
    array('pokemon_count[-]' => 1), 
    array('id' => 99)
);

Executing the code above won’t make medoo complain about it. It just fails silently. To check for the error we need to call the error method right after the offending code:

$db->error();

This returns an array which look like the following:

Array ( 
    [0] => 42S02 
    [1] => 1146 
    [2] => Table 'pokemon.trainerw' doesn't exist 
)

The first item is the error code returned by MySQL. You can see a list of error codes on this page. The second item is the SQL state. And the third item is a human readable description of the error.

If the error is an error with the arguments passed to the specific method that you’re using, medoo returns an error so you won’t really need to check for those if it happens.

If there aren’t any errors returned when you call the error method, then it might not be an error. Instead, it can be that the query generated by medoo doesn’t do what you want it to do. In those cases you need to use the last_query() method. As the name suggests, calling this method will return the last query executed by medoo. Let’s take a look at some examples:

  • Decrease the pokemon_count by 1 for trainer with the id of 1:
$db->update(
    'trainers', 
    array('pokemon_count[-]' => 1), 
    array('id' => 1)
);

echo $db->last_query();

/*
returns: 
UPDATE "trainers" SET "pokemon_count" = "pokemon_count" - 1 WHERE "id" = 1
*/
  • Select the pokemon with the name ‘virizion’:
$db->get(
    'pokemon', 
    array('name', 'level', 'type'), array('name' => 'virizion')
);

echo $db->last_query();
/*
returns:
SELECT "name","level","type" FROM "pokemon" WHERE "name" = 'virizion' LIMIT 1
*/
  • Select all pokemon with their corresponding type:
$db->select(
    'pokemon', 
    array('[><]types' => array('type_id' => 'id')),
    array('pokemon.name', 'types.name(type)')
);

echo $db->last_query();
/*
returns:
SELECT "pokemon"."name","types"."name" AS "type" FROM "pokemon" INNER JOIN "types" ON "pokemon"."type_id" = "types"."id"
*/

Conclusion

That’s it! In this tutorial we have learned about Medoo, a lightweight and easy to use database abstraction library for PHP. We learned how to select, update, delete, and insert data into the database. We also learned some debugging techniques that we can use if something goes wrong. If you want to learn more, check out meedo’s official documentation.

Are you using Medoo for anything? What do you think about the project? Let us know!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
Michael_Morris

I read the article, if that's all there is to this library it's a complete waste of time. It offers no advantages over straight SQL if you know what you're doing. You end up spending time learning their syntax instead of learning PDO + SQL's syntax to do the same thing. For example how is this?

$db->update(    
  'trainers',     
  array('pokemon_count[+]' => 3),     
  array('id' => $trainer_id)
);

Any better than this?

$pdo->prepare( 'UPDATE trainers SET pokemon_count = pokemon_count + ?, id = ?')->execute([3,$trainer_id]);

I've written a better PDO wrapper than this and actually posted it on the old forum.

Petah

Agree 100%

Why bother abstracting away SQL?

I do like a little sugar like:

$db->write('
    UPDATE trainers
    SET pokemon_count = pokemon_count + :amount
    WHERE id = :id;
', [
    ':id' => $this->getID(),
    ':amount' => 69,
]);
TaylorRen

Well, either I use a very sophisticated DBAL, like Propel, Doctrine, or I go directly with Native SQL.

A Lisp-like grammar "AND(a, b)" is NOT intuitive at all. It may be easier for the wrapper to parse, but it is against all human reading and SQL grammar presentation.

derak

This looks a lot like Zend Framework 1.x's ZendDb layer.
If your query is built using complex logic, abstracting the query into an object is pretty helpful.

I probably won't be using this.
This is too complex for the simple case and not that different from Zend Db.

Hulkur

Serious missing feature - get generated sql without actually running it.
last_query() is good, getSql() is better.

roman

Michael, would you mind sharing the latest version of your PDO wrapper?

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.