How to Bind to Eloquent Model Event in Laravel 5

Whenever a Laravel model is modified there are a number of events that fire that allow you to trigger your own action(s). For example, in my PoliticsEQ application I needed to calculate some statistics about keywords and sentiment scores and store them in a separate table to improve performance for some of the front in graphs. The challenge was making sure the keyword statistics table was updated whenever a keyword was added or updated.

Laravel gives you the following events right out of the box: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored. You can get the particulars here. Most of them are pretty intuitive.

In my case it made sense to use the “saved” event. To make this happen was remarkably. All I had to do was create a new “Service Provider” and use the Event::saved pattern in the provider’s boot method.

So first I created a KeywordStats provider.


namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Keyword, App\KeywordStat;
use Log;

class KeywordStats extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        Keyword::saved(function($keyword) {
          $stat = KeywordStat::where('keyword_name', $keyword->name)->first();
          if (!$stat) {
            $stat = new KeywordStat(['keyword_name'=>$keyword->name]);
          }
          $stat->sentiment_avg = Keyword::where('name', $keyword->name)->avg('sentiment_score');
          $stat->total_usages = Keyword::where('name',$keyword->name)->get()->count();
          $stat->save();
          Log::info("Updated {$keyword->name} / avg: {$stat->sentiment_avg} / count: {$stat->total_usages}");
        });
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        
    }
}

Then I simply registered the provider in the config/app.php providers array:


    App\Providers\KeywordStats::class,

Covert all tables from MyISAM to InnoDB

Here’s a quick one liner to convert all tables on in a specified database from MyISAM to InnoDB:

mysql -Bse "SELECT CONCAT('ALTER TABLE ',table_schema,'.',table_name,' engine=InnoDB;') FROM information_schema.tables WHERE engine = 'MyISAM' and table_schema = 'your_database_name';" | xargs -I {} mysql -e {}

Boom! You’re welcome.

Hide WordPress Post from All Queries

Problem: you want to create a variation of a page but you don’t want it to show up on the home page or in any archives or anything. You just need a direct link so you can share it with someone.

Solution:

add_action('pre_get_posts', 'hide_hidden_posts');
function hide_hidden_posts($query) {
  if ( is_admin() ) {
    return $query;
  }

  if ( is_single() AND $query->is_main_query() ) {
    return $query;
  }
  $ids = wp_cache_get('hidden_posts', 'posts');
  if ( !$ids ) {
    global $wpdb;
    $ids = $wpdb->get_col("SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = 'hide_post'");
    wp_cache_set('hidden_posts', $ids, 'posts');
  }
  $query->set('post__not_in', $ids);
  return $query;
}

This function will modify all WordPress’ frontend queries to exclude any posts with a custom field “hide_post”, except in the case that the query is the main query on a single post page.

Caveat: This will only be functional for plugins and themes using the WP_Query api. Custom queries will not be modified.

WordPress Hack: Reorder the Admin Menu

For many small business clients pages wind up being the most important WordPress post type. Many of them don’t even want a blog, just a nice looking site where they can update the content periodically, i.e. the contact page, the services page etc.

So for these clients it’s often a little confusing to have “Posts” at the top of the nav menu. Here’s how you would reorder to the nav to put pages on top.

First turn on the custom ordering option:

add_filter('custom_menu_order','__return_true');

Then create a function to filter the menu order array. In the function below we’re using array_splice to cut out the menu starting at position 2 because the “posts” menu item is #3 by default. How did I know that? Just add print_r($menu) and you’ll get a dump of the menu order. After splicing the array, we iterate over the old menu array, the part we extracted ($old), using a foreach loop adding the old menu items back in.

function mc_menu_order($menu) {
// more on array_splice here: https://php.net/manual/en/function.array-splice.php
$old = array_splice($menu,2,count($menu),array('2'=>'edit.php?post_type=page'));
$keys = array_flip($menu);
foreach($old as $item) {
if(!array_key_exists($item,$keys)) {
$menu[] = $item;
}
}
unset($old);
unset($keys);
return $menu;
}

Then just add the api call to insert that function:

add_filter('menu_order','mc_menu_order');

Here it is all together:

//menu reorder
add_filter('custom_menu_order','__return_true');
add_filter('menu_order','mc_menu_order');
function mc_menu_order($menu) {
$old = array_splice($menu,2,count($menu),array('2'=>'edit.php?post_type=page'));
$keys = array_flip($menu);
foreach($old as $item) {
if(!array_key_exists($item,$keys)) {
$menu[] = $item;
}
}
unset($old);
unset($keys);
return $menu;
}

WordPress Portable Database Caching Class

So here’s a little library/class that I wrote to make caching a little easier from project to project. WordPress requires a plugin like W3 Total Cache to be in place for “persistent” caching to be available … that is, caches of data that survive longer than the current page view. But sometimes when you’re building a complex custom project there are some queries you know could be safely cached regardless of the global caching setup. With this class you can build it right into your theme or plugin.

WordPress $current_user Global Changed in WordPress 3.3

WordPress has been slowly changing their global $current_user object and the changes in WordPress 3.3 broke my Simplr Registration Form Plus plugin and probably a few others. For any other developers trying to investigate why usermeta field values disappear it is because:

global $current_user;

This was used to return an object containing not only the primary user fields but all the meta fields as well. So if you set a user meta field called “age” you could simply do:

global $current_user;
if($current_user->data->age > 21) {
  // the do some adult stuff;
}

But this shortcut is no longer available to developers and for good reason. As a general rule code should be efficient, meaning it does only what it has to. To load even the most superflous meta fields every time you access the basic user object is a waste of resources.

So good for WordPress, they are improving. But there are likely others like me who figured if WordPress was going to give me the info, then I was going to use it. And like me they’ll have to spend all day tracking down everywhere they used this shortcut and fixing it. Ugh.

The Simplest WordPress User Access Log Ever

Those of use who develop using pods often find we use it for everything. So here’s a quick tip on using PodsCMS to create a custom user access log.

Step 1: Set up the Pod

I’m assuming you’ve already installed/activate both the PodsCMS and Pods UI plugins. If not, please do so before starting.

Create a new Pod called “logins”.

By default each pod is created with a name and slug field. We’re going to use the name field but you can delete the slug field.

Then you’ll need to create a field for “date”. Of course, Pods stores the date any entry is created in a field called “created” which you can access from within Pods Templates. But it still makes sense to have a date field in the Pod itself, if nothing else for the sake of a clear data model.

So once you have added the date field your Pod will look like this:

Step 2: Add function

Now just add the following code to your functions.php file.

add_action('set_logged_in_cookie','mpv_add_access_log_entry');
function mpv_add_access_log_entry($user) {
$user = explode('|',$user);
$log = new PodAPI(); 
$params = array('datatype'=>'logins');
$params['columns'] = array('name'=>$user[0],'date'=>date('Y-m-d H:i:s'));
$log->save_pod_item(pods_sanitize($params));
}

This function uses the Pod API to insert a row in the logins table. Alternatively you can use the $wpdb class and do something like this:

global $wpdb;
$wpdb->insert($wpdb->prefix.'pod_tbl_logins',array('name'=> $user[0],'date'=> date('Y-m-d H:i:s')));

The only trouble with going this route is that in order to use the pods admin interface to manage the data, you’ll also need to add a row to the wp_pod table. This will change with Pods 2.0 so there’s no need to demonstrate. But I strongly recommend using the PodsAPI class as it will make sure to implement best practices and in 2.0 it will use the $wpdb class anyway.

So that’s it. To create an exportable report of the logins just install the “Exports and Reports” plugin. Or you can use PodsUI to create a custom interface.

Have fun.

WordPress Hack #1: Global Meta Variable for Custom Fields

This is the first in what I hope will be a series of posts on “WordPress Hacks”, simple code you can add to functions.php to make your life as a developer a little easier.

Ever get tired of typing get_post_meta($post->ID, $meta_key, $true); to fetch even the simplest of WordPress custom field values. Try this.

add_action('the_post','setup_meta_var');
function setup_meta_var() {
global $wp_query,$meta;
$vals = get_post_custom($data->ID);
foreach($vals as $k => $v) 
{
if(count($v) > 1):
$meta[$k] = $v;
else:
$meta[$k] = $v[0];
endif;
}
}

What this dandy little function does is automatically assign a post or page’s custom values to a global variable call $meta.

To use your custom field values now, you simply have the do echo the meta_key like so:

global $meta;
echo $meta['meta_key'];

If there is more than one value for that custom field, you will simply need to do a standard foreach loop.

global $meta;
foreach($meta['meta_key'] as $m) {
echo $m;
}

Update: Hey, I realized that in the code snippet I pasted here I was setting a $type variable. That was a relic of the custom script this code was taken from … so please ignore.