Ruby on Rails’ ActiveRecord class has some really neat features, none more so perhaps than the incredibly useful find_by_attributes method. With object-oriented PHP programming we can bring that functionality to our PHP applications using PHP’s magic methods.
Create some classes
To get started, we’ll need our own ActiveRecord class. Let’s call it ActiveRecord, that seems like a sensible idea!
1 2 3 4 5 6 |
<?php class ActiveRecord { } |
Now we want to call this method when we’re looking for pages, or other content, so we’ll also create a Page class that extends this ActiveRecord class:
1 2 3 4 5 6 |
<?php class Page extends ActiveRecord { } |
Using method overloading
For this to work we’re going to use some method overloading – that essentially means we’re going to force PHP to look at a magic method we write before trying to find a function with the called method name in our classes. To do this we’ll be using the method __callStatic like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static function __callStatic($method, $arguments) { // Check if we've called a method starting with 'find_by_' if (strpos($method, 'find_by_') === 0) { // We did, so let's do something here } else { // Otherwise, continue on as before return parent::__callStatic($method, $arguments); } } |
So let’s break look at how we might want to use this… in Ruby on Rails, if you want to find a page that has an enabled flag set to 1 and a unique identifier like a handle, you might use:
1 |
Page.find_by_handle_and_enabled('my-page', 1) |
So in PHP, our equivalent will be:
1 |
Page::find_by_handle_and_enabled('my-page', 1); |
Getting the key/value pairs for our query
Now we’ll need to add some logic into our ActiveRecord class to break up the method name and use it with the arguments to get the combination of key/value pairs:
1 2 3 4 5 6 |
// Get the fields we are looking at $field_string = str_replace('find_by_', '', $method); $field_array = explode('_', $field_string); // Combine the fields with the values $field_value_pairs = array_combine($field_array, $parameters); |
Working out the database table name with late static binding
After doing the above, we now have an associative array of fields and their values which we can use in our SQL query. What we need to work out now is what the table name is. How you do this is up to you, personally I favour using singular table names that are underscore-separated equivalents to their CamelCase class name counterparts, so for the “Page” class it would simply be “page”, and for classes like “SalesOrder” the table name would be “sales_order”. If you want to save time you can always assign a table name variable to each class and use self::$table_name to retrieve it. If however you’d like to do it the way I describe above, you’ll need a function in your ActiveRecord class to convert your CamelCase strings into underscore-separated table names:
1 2 3 4 5 6 7 8 9 |
public static function camel_to_underscore($string) { // First, add underscores before all uppercase letters $underscored = preg_replace('/([A-Z])/', '_$1', $string); // Now remove the underscore at the start of your string $trimmed = ltrim($underscored, '_'); // And return it in lowercase return strtolower($trimmed); // return lowercase version } |
We can now use this function in conjunction with PHP’s late static binding to get the table name for the class we’re using:
1 2 |
$class = get_called_class(); $table_name = self::camel_to_underscore($class); |
Getting some results
Now all we need to do is build a SQL query to fetch the result(s) for our conditions from the table in question – here’s a MySQL example – and to return the results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
$mysqli = new mysqli('localhost', 'mysql_user', 'mysql_pw', 'my_database'); // Create the SQL conditions $conditions = array(); foreach ($field_value_pairs as $field => $value) { $conditions[] = "$field = '" . $mysqli->real_escape_string($value) . "'"; } // Now the full query $sql = "SELECT $table_name.* FROM $table_name WHERE (" . implode(' AND ', $conditions) . ")"; // Perform the query if ($result = $mysqli->query($sql)) { // Create an empty array to return the objects $objects = array(); // Loop over the results while ($row = $result->fetch_assoc()) { // Create a new object of the called class $object = new $class; // Assign the row values to it foreach ($row as $field => $value) { $object->$field = $value; } // Add it to our objects array $objects[] = $object; } // Free the result $result->close(); // Close the db connection $mysqli->close(); return $objects; // Return the objects array } else { // Some MySQL error handling can go here } |
Putting it all together
We now have all we need in order to use Ruby on Rails style ActiveRecord find_by_attributes methods with our PHP classes; the completed ActiveRecord class should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<?php class ActiveRecord { public static function __callStatic($method, $arguments) { // Check if we've called a method starting with 'find_by_' if (strpos($method, 'find_by_') === 0) { // Get the fields we are looking at $field_string = str_replace('find_by_', '', $method); $field_array = explode('_', $field_string); // Combine the fields with the values $field_value_pairs = array_combine($field_array, $parameters); // Get the table name based on the class name $class = get_called_class(); $table_name = self::camel_to_underscore($class); // Open a database connection $mysqli = new mysqli('localhost', 'mysql_user', 'mysql_pw', 'my_database'); // Create the SQL conditions $conditions = array(); foreach ($field_value_pairs as $field => $value) { $conditions[] = "$field = '" . $mysqli->real_escape_string($value) . "'"; } // Now the full query $sql = "SELECT $table_name.* FROM $table_name WHERE (" . implode(' AND ', $conditions) . ")"; // Perform the query if ($result = $mysqli->query($sql)) { // Create an empty array to return the objects $objects = array(); // Loop over the results while ($row = $result->fetch_assoc()) { // Create a new object of the called class $object = new $class; // Assign the row values to it foreach ($row as $field => $value) { $object->$field = $value; } // Add it to our objects array $objects[] = $object; } // Free the result $result->close(); // Close the db connection $mysqli->close(); return $objects; // Return the objects array } else { // Some MySQL error handling can go here } } else { // Otherwise, continue on as before return parent::__callStatic($method, $arguments); } } public static function camel_to_underscore($string) { // First, add underscores before all uppercase letters $underscored = preg_replace('/([A-Z])/', '_$1', $string); // Now remove the underscore at the start of your string $trimmed = ltrim($underscored, '_'); // And return it in lowercase return strtolower($trimmed); // return lowercase version } } |
Taking it further
If you want to build on this you could introduce some error handling – the above would return an empty array if no results were found, but maybe you’d prefer to always assume they’re looking for one unique object, so you could throw an Exception if there was no unique object found using the given parameters.